├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── me │ │ └── kyuubiran │ │ └── ncmdumper │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── cpp │ │ ├── CMakeLists.txt │ │ └── src │ │ │ ├── AES.cpp │ │ │ ├── AES.h │ │ │ ├── json.hpp │ │ │ ├── ncm.cpp │ │ │ ├── ncm.h │ │ │ └── ncmdumper.cpp │ ├── java │ │ └── me │ │ │ └── kyuubiran │ │ │ └── ncmdumper │ │ │ ├── MainActivity.kt │ │ │ ├── MyApplication.kt │ │ │ └── ui │ │ │ ├── pages │ │ │ ├── MainPage.kt │ │ │ └── SettingsPage.kt │ │ │ ├── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ │ ├── utils │ │ │ ├── DpUtils.kt │ │ │ └── Dumper.kt │ │ │ └── views │ │ │ └── NcmFileItem.kt │ └── res │ │ ├── drawable │ │ ├── baseline_arrow_back_24.xml │ │ ├── baseline_more_vert_24.xml │ │ ├── baseline_refresh_24.xml │ │ ├── baseline_settings_24.xml │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── me │ └── kyuubiran │ └── ncmdumper │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /.idea/ 17 | /app/release/ 18 | /app/debug/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NcmDumperApp 2 | 从某数字音乐加密文件中导出mp3/flac等格式的音乐 3 | **注:一切开发旨在学习,请勿用于非法用途** 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidApplication) 3 | alias(libs.plugins.jetbrainsKotlinAndroid) 4 | } 5 | 6 | @Suppress("UnstableApiUsage") 7 | android { 8 | namespace = "me.kyuubiran.ncmdumper" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | applicationId = "me.kyuubiran.ncmdumper" 13 | minSdk = 24 14 | targetSdk = 34 15 | versionCode = 4 16 | versionName = "0.4" 17 | 18 | buildFeatures { 19 | buildConfig = true 20 | } 21 | 22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 23 | vectorDrawables { 24 | useSupportLibrary = true 25 | } 26 | externalNativeBuild { 27 | cmake { 28 | val flags = arrayOf( 29 | "-Wignored-attributes", 30 | ) 31 | 32 | cppFlags("-std=c++2a", *flags) 33 | cFlags("-std=c18", *flags) 34 | } 35 | } 36 | } 37 | 38 | buildTypes { 39 | release { 40 | isMinifyEnabled = false 41 | proguardFiles( 42 | getDefaultProguardFile("proguard-android-optimize.txt"), 43 | "proguard-rules.pro" 44 | ) 45 | 46 | externalNativeBuild { 47 | cmake { 48 | val releaseFlags = arrayOf( 49 | "-O3 -DNDEBUG" 50 | ) 51 | 52 | cppFlags += releaseFlags 53 | cFlags += releaseFlags 54 | } 55 | } 56 | } 57 | } 58 | compileOptions { 59 | sourceCompatibility = JavaVersion.VERSION_11 60 | targetCompatibility = JavaVersion.VERSION_11 61 | } 62 | kotlinOptions { 63 | jvmTarget = "11" 64 | } 65 | buildFeatures { 66 | compose = true 67 | } 68 | composeOptions { 69 | kotlinCompilerExtensionVersion = "1.5.1" 70 | } 71 | packaging { 72 | resources { 73 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 74 | } 75 | } 76 | externalNativeBuild { 77 | cmake { 78 | path = file("src/main/cpp/CMakeLists.txt") 79 | version = "3.22.1" 80 | } 81 | } 82 | } 83 | 84 | dependencies { 85 | implementation(libs.androidx.core.ktx) 86 | implementation(libs.androidx.lifecycle.runtime.ktx) 87 | implementation(libs.androidx.activity.compose) 88 | implementation(platform(libs.androidx.compose.bom)) 89 | implementation(libs.androidx.ui) 90 | implementation(libs.androidx.ui.graphics) 91 | implementation(libs.androidx.ui.tooling.preview) 92 | implementation(libs.androidx.material) 93 | implementation(libs.androidx.activity.compose) 94 | implementation(libs.androidx.navigation.compose) 95 | 96 | implementation(libs.accompanist.systemuicontroller) 97 | 98 | testImplementation(libs.junit) 99 | androidTestImplementation(libs.androidx.junit) 100 | androidTestImplementation(libs.androidx.espresso.core) 101 | androidTestImplementation(platform(libs.androidx.compose.bom)) 102 | androidTestImplementation(libs.androidx.ui.test.junit4) 103 | debugImplementation(libs.androidx.ui.tooling) 104 | debugImplementation(libs.androidx.ui.test.manifest) 105 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/me/kyuubiran/ncmdumper/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.ncmdumper 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("me.kyuubiran.ncmdumper", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | # For more information about using CMake with Android Studio, read the 3 | # documentation: https://d.android.com/studio/projects/add-native-code.html. 4 | # For more examples on how to use CMake, see https://github.com/android/ndk-samples. 5 | 6 | # Sets the minimum CMake version required for this project. 7 | cmake_minimum_required(VERSION 3.22.1) 8 | 9 | # Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, 10 | # Since this is the top level CMakeLists.txt, the project name is also accessible 11 | # with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level 12 | # build script scope). 13 | project("ncmdumper") 14 | 15 | # Creates and names a library, sets it as either STATIC 16 | # or SHARED, and provides the relative paths to its source code. 17 | # You can define multiple libraries, and CMake builds them for you. 18 | # Gradle automatically packages shared libraries with your APK. 19 | # 20 | # In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define 21 | # the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} 22 | # is preferred for the same purpose. 23 | # 24 | # In order to load a library into your app from Java/Kotlin, you must call 25 | # System.loadLibrary() and pass the name of the library defined here; 26 | # for GameActivity/NativeActivity derived applications, the same library name must be 27 | # used in the AndroidManifest.xml file. 28 | add_library(${CMAKE_PROJECT_NAME} SHARED 29 | # List C/C++ source files with relative paths to this CMakeLists.txt. 30 | src/ncmdumper.cpp src/AES.cpp src/ncm.cpp) 31 | 32 | # Specifies libraries CMake should link to your target library. You 33 | # can link libraries from various origins, such as libraries defined in this 34 | # build script, prebuilt third-party libraries, or Android system libraries. 35 | target_link_libraries(${CMAKE_PROJECT_NAME} 36 | # List libraries link to the target library 37 | android 38 | log) 39 | -------------------------------------------------------------------------------- /app/src/main/cpp/src/AES.cpp: -------------------------------------------------------------------------------- 1 | #include "AES.h" 2 | 3 | AES::AES(const AESKeyLength keyLength) { 4 | switch (keyLength) { 5 | case AESKeyLength::AES_128: 6 | this->Nk = 4; 7 | this->Nr = 10; 8 | break; 9 | case AESKeyLength::AES_192: 10 | this->Nk = 6; 11 | this->Nr = 12; 12 | break; 13 | case AESKeyLength::AES_256: 14 | this->Nk = 8; 15 | this->Nr = 14; 16 | break; 17 | } 18 | } 19 | 20 | std::vector AES::EncryptECB(const uint8_t in[], size_t inLen, 21 | const uint8_t key[]) { 22 | CheckLength(inLen); 23 | auto out = std::vector(inLen); 24 | auto roundKeys = std::vector(4 * Nb * (Nr + 1)); 25 | KeyExpansion(key, roundKeys.data()); 26 | for (size_t i = 0; i < inLen; i += blockBytesLen) { 27 | EncryptBlock(in + i, out.data() + i, roundKeys.data()); 28 | } 29 | 30 | return out; 31 | } 32 | 33 | std::vector AES::DecryptECB(const uint8_t in[], size_t inLen, 34 | const uint8_t key[]) { 35 | CheckLength(inLen); 36 | auto out = std::vector(inLen); 37 | auto roundKeys = std::vector(4 * Nb * (Nr + 1)); 38 | KeyExpansion(key, roundKeys.data()); 39 | for (size_t i = 0; i < inLen; i += blockBytesLen) { 40 | DecryptBlock(in + i, out.data() + i, roundKeys.data()); 41 | } 42 | 43 | return out; 44 | } 45 | 46 | std::vector AES::EncryptCBC(const uint8_t in[], size_t inLen, 47 | const uint8_t key[], 48 | const uint8_t *iv) { 49 | CheckLength(inLen); 50 | auto out = std::vector(inLen); 51 | uint8_t block[blockBytesLen]; 52 | auto roundKeys = std::vector(4 * Nb * (Nr + 1)); 53 | KeyExpansion(key, roundKeys.data()); 54 | memcpy(block, iv, blockBytesLen); 55 | for (size_t i = 0; i < inLen; i += blockBytesLen) { 56 | XorBlocks(block, in + i, block, blockBytesLen); 57 | EncryptBlock(block, out.data() + i, roundKeys.data()); 58 | memcpy(block, out.data() + i, blockBytesLen); 59 | } 60 | 61 | return out; 62 | } 63 | 64 | std::vector AES::DecryptCBC(const uint8_t in[], size_t inLen, 65 | const uint8_t key[], 66 | const uint8_t *iv) { 67 | CheckLength(inLen); 68 | auto out = std::vector(inLen); 69 | uint8_t block[blockBytesLen]; 70 | auto roundKeys = std::vector(4 * Nb * (Nr + 1)); 71 | KeyExpansion(key, roundKeys.data()); 72 | memcpy(block, iv, blockBytesLen); 73 | for (size_t i = 0; i < inLen; i += blockBytesLen) { 74 | DecryptBlock(in + i, out.data() + i, roundKeys.data()); 75 | XorBlocks(block, out.data() + i, out.data() + i, blockBytesLen); 76 | memcpy(block, in + i, blockBytesLen); 77 | } 78 | 79 | return out; 80 | } 81 | 82 | std::vector AES::EncryptCFB(const uint8_t in[], size_t inLen, 83 | const uint8_t key[], 84 | const uint8_t *iv) { 85 | CheckLength(inLen); 86 | auto out = std::vector(inLen); 87 | uint8_t block[blockBytesLen]; 88 | auto roundKeys = std::vector(4 * Nb * (Nr + 1)); 89 | KeyExpansion(key, roundKeys.data()); 90 | memcpy(block, iv, blockBytesLen); 91 | for (size_t i = 0; i < inLen; i += blockBytesLen) { 92 | uint8_t encryptedBlock[blockBytesLen]; 93 | EncryptBlock(block, encryptedBlock, roundKeys.data()); 94 | XorBlocks(in + i, encryptedBlock, out.data() + i, blockBytesLen); 95 | memcpy(block, out.data() + i, blockBytesLen); 96 | } 97 | 98 | return out; 99 | } 100 | 101 | std::vector AES::DecryptCFB(const uint8_t in[], size_t inLen, 102 | const uint8_t key[], 103 | const uint8_t *iv) { 104 | CheckLength(inLen); 105 | auto out = std::vector(inLen); 106 | uint8_t block[blockBytesLen]; 107 | auto roundKeys = std::vector(4 * Nb * (Nr + 1)); 108 | KeyExpansion(key, roundKeys.data()); 109 | memcpy(block, iv, blockBytesLen); 110 | for (size_t i = 0; i < inLen; i += blockBytesLen) { 111 | uint8_t encryptedBlock[blockBytesLen]; 112 | EncryptBlock(block, encryptedBlock, roundKeys.data()); 113 | XorBlocks(in + i, encryptedBlock, out.data() + i, blockBytesLen); 114 | memcpy(block, in + i, blockBytesLen); 115 | } 116 | 117 | return out; 118 | } 119 | 120 | void AES::CheckLength(const size_t len) { 121 | if (len % blockBytesLen != 0) { 122 | throw std::length_error("Plaintext length must be divisible by " + 123 | std::to_string(blockBytesLen)); 124 | } 125 | } 126 | 127 | void AES::EncryptBlock(const uint8_t in[], uint8_t out[], uint8_t *roundKeys) const { 128 | uint8_t state[4][Nb]; 129 | size_t i, j, round; 130 | 131 | for (i = 0; i < 4; i++) { 132 | for (j = 0; j < Nb; j++) { 133 | state[i][j] = in[i + 4 * j]; 134 | } 135 | } 136 | 137 | AddRoundKey(state, roundKeys); 138 | 139 | for (round = 1; round <= Nr - 1; round++) { 140 | SubBytes(state); 141 | ShiftRows(state); 142 | MixColumns(state); 143 | AddRoundKey(state, roundKeys + round * 4 * Nb); 144 | } 145 | 146 | SubBytes(state); 147 | ShiftRows(state); 148 | AddRoundKey(state, roundKeys + Nr * 4 * Nb); 149 | 150 | for (i = 0; i < 4; i++) { 151 | for (j = 0; j < Nb; j++) { 152 | out[i + 4 * j] = state[i][j]; 153 | } 154 | } 155 | } 156 | 157 | void AES::DecryptBlock(const uint8_t in[], uint8_t out[], 158 | uint8_t *roundKeys) const { 159 | uint8_t state[4][Nb]; 160 | size_t i, j, round; 161 | 162 | for (i = 0; i < 4; i++) { 163 | for (j = 0; j < Nb; j++) { 164 | state[i][j] = in[i + 4 * j]; 165 | } 166 | } 167 | 168 | AddRoundKey(state, roundKeys + Nr * 4 * Nb); 169 | 170 | for (round = Nr - 1; round >= 1; round--) { 171 | InvSubBytes(state); 172 | InvShiftRows(state); 173 | AddRoundKey(state, roundKeys + round * 4 * Nb); 174 | InvMixColumns(state); 175 | } 176 | 177 | InvSubBytes(state); 178 | InvShiftRows(state); 179 | AddRoundKey(state, roundKeys); 180 | 181 | for (i = 0; i < 4; i++) { 182 | for (j = 0; j < Nb; j++) { 183 | out[i + 4 * j] = state[i][j]; 184 | } 185 | } 186 | } 187 | 188 | void AES::SubBytes(uint8_t state[4][Nb]) { 189 | size_t i, j; 190 | uint8_t t; 191 | for (i = 0; i < 4; i++) { 192 | for (j = 0; j < Nb; j++) { 193 | t = state[i][j]; 194 | state[i][j] = sbox[t / 16][t % 16]; 195 | } 196 | } 197 | } 198 | 199 | void AES::ShiftRow(uint8_t state[4][Nb], size_t i, 200 | size_t n) // shift row i on n positions 201 | { 202 | uint8_t tmp[Nb]; 203 | for (size_t j = 0; j < Nb; j++) { 204 | tmp[j] = state[i][(j + n) % Nb]; 205 | } 206 | memcpy(state[i], tmp, Nb * sizeof(uint8_t)); 207 | } 208 | 209 | void AES::ShiftRows(uint8_t state[4][Nb]) { 210 | ShiftRow(state, 1, 1); 211 | ShiftRow(state, 2, 2); 212 | ShiftRow(state, 3, 3); 213 | } 214 | 215 | uint8_t AES::xtime(uint8_t b) // multiply on x 216 | { 217 | return (b << 1) ^ (((b >> 7) & 1) * 0x1b); 218 | } 219 | 220 | void AES::MixColumns(uint8_t state[4][Nb]) { 221 | uint8_t temp_state[4][Nb]; 222 | 223 | for (auto &i: temp_state) { 224 | memset(i, 0, 4); 225 | } 226 | 227 | for (size_t i = 0; i < 4; ++i) { 228 | for (size_t k = 0; k < 4; ++k) { 229 | for (size_t j = 0; j < 4; ++j) { 230 | if (CMDS[i][k] == 1) 231 | temp_state[i][j] ^= state[k][j]; 232 | else 233 | temp_state[i][j] ^= GF_MUL_TABLE[CMDS[i][k]][state[k][j]]; 234 | } 235 | } 236 | } 237 | 238 | for (size_t i = 0; i < 4; ++i) { 239 | memcpy(state[i], temp_state[i], 4); 240 | } 241 | } 242 | 243 | void AES::AddRoundKey(uint8_t state[4][Nb], const uint8_t *key) { 244 | size_t i, j; 245 | for (i = 0; i < 4; i++) { 246 | for (j = 0; j < Nb; j++) { 247 | state[i][j] = state[i][j] ^ key[i + 4 * j]; 248 | } 249 | } 250 | } 251 | 252 | void AES::SubWord(uint8_t *a) { 253 | int i; 254 | for (i = 0; i < 4; i++) { 255 | a[i] = sbox[a[i] / 16][a[i] % 16]; 256 | } 257 | } 258 | 259 | void AES::RotWord(uint8_t *a) { 260 | uint8_t c = a[0]; 261 | a[0] = a[1]; 262 | a[1] = a[2]; 263 | a[2] = a[3]; 264 | a[3] = c; 265 | } 266 | 267 | void AES::XorWords(const uint8_t *a, const uint8_t *b, uint8_t *c) { 268 | int i; 269 | for (i = 0; i < 4; i++) { 270 | c[i] = a[i] ^ b[i]; 271 | } 272 | } 273 | 274 | void AES::Rcon(uint8_t *a, const size_t n) { 275 | size_t i; 276 | uint8_t c = 1; 277 | for (i = 0; i < n - 1; i++) { 278 | c = xtime(c); 279 | } 280 | 281 | a[0] = c; 282 | a[1] = a[2] = a[3] = 0; 283 | } 284 | 285 | void AES::KeyExpansion(const uint8_t key[], uint8_t w[]) const { 286 | uint8_t temp[4]; 287 | uint8_t rcon[4]; 288 | 289 | size_t i = 0; 290 | while (i < 4 * Nk) { 291 | w[i] = key[i]; 292 | i++; 293 | } 294 | 295 | i = 4 * Nk; 296 | while (i < 4 * Nb * (Nr + 1)) { 297 | temp[0] = w[i - 4 + 0]; 298 | temp[1] = w[i - 4 + 1]; 299 | temp[2] = w[i - 4 + 2]; 300 | temp[3] = w[i - 4 + 3]; 301 | 302 | if (i / 4 % Nk == 0) { 303 | RotWord(temp); 304 | SubWord(temp); 305 | Rcon(rcon, i / (Nk * 4)); 306 | XorWords(temp, rcon, temp); 307 | } else if (Nk > 6 && i / 4 % Nk == 4) { 308 | SubWord(temp); 309 | } 310 | 311 | w[i + 0] = w[i - 4 * Nk] ^ temp[0]; 312 | w[i + 1] = w[i + 1 - 4 * Nk] ^ temp[1]; 313 | w[i + 2] = w[i + 2 - 4 * Nk] ^ temp[2]; 314 | w[i + 3] = w[i + 3 - 4 * Nk] ^ temp[3]; 315 | i += 4; 316 | } 317 | } 318 | 319 | void AES::InvSubBytes(uint8_t state[4][Nb]) { 320 | size_t i, j; 321 | uint8_t t; 322 | for (i = 0; i < 4; i++) { 323 | for (j = 0; j < Nb; j++) { 324 | t = state[i][j]; 325 | state[i][j] = inv_sbox[t / 16][t % 16]; 326 | } 327 | } 328 | } 329 | 330 | void AES::InvMixColumns(uint8_t state[4][Nb]) { 331 | uint8_t temp_state[4][Nb]; 332 | 333 | for (auto &i: temp_state) { 334 | memset(i, 0, 4); 335 | } 336 | 337 | for (size_t i = 0; i < 4; ++i) { 338 | for (size_t k = 0; k < 4; ++k) { 339 | for (size_t j = 0; j < 4; ++j) { 340 | temp_state[i][j] ^= GF_MUL_TABLE[INV_CMDS[i][k]][state[k][j]]; 341 | } 342 | } 343 | } 344 | 345 | for (size_t i = 0; i < 4; ++i) { 346 | memcpy(state[i], temp_state[i], 4); 347 | } 348 | } 349 | 350 | void AES::InvShiftRows(uint8_t state[4][Nb]) { 351 | ShiftRow(state, 1, Nb - 1); 352 | ShiftRow(state, 2, Nb - 2); 353 | ShiftRow(state, 3, Nb - 3); 354 | } 355 | 356 | void AES::XorBlocks(const uint8_t *a, const uint8_t *b, 357 | uint8_t *c, const size_t len) { 358 | for (size_t i = 0; i < len; i++) { 359 | c[i] = a[i] ^ b[i]; 360 | } 361 | } 362 | 363 | void AES::printHexArray(uint8_t a[], const size_t n) { 364 | for (size_t i = 0; i < n; i++) { 365 | printf("%02x ", a[i]); 366 | } 367 | } 368 | 369 | void AES::printHexVector(const std::vector &a) { 370 | for (const auto i: a) { 371 | printf("%02x ", i); 372 | } 373 | } 374 | 375 | std::vector AES::ArrayToVector(uint8_t *a, 376 | const size_t len) { 377 | std::vector v(a, a + len * sizeof(uint8_t)); 378 | return v; 379 | } 380 | 381 | uint8_t* AES::VectorToArray(std::vector &a) { 382 | return a.data(); 383 | } 384 | 385 | std::vector AES::EncryptECB(std::vector in, 386 | std::vector key) { 387 | return EncryptECB(VectorToArray(in), in.size(), VectorToArray(key)); 388 | } 389 | 390 | std::vector AES::DecryptECB(std::vector in, 391 | std::vector key) { 392 | return DecryptECB(VectorToArray(in), in.size(), VectorToArray(key)); 393 | } 394 | 395 | std::vector AES::EncryptCBC(std::vector in, 396 | std::vector key, 397 | std::vector iv) { 398 | return EncryptCBC(VectorToArray(in), in.size(), VectorToArray(key), VectorToArray(iv)); 399 | } 400 | 401 | std::vector AES::DecryptCBC(std::vector in, 402 | std::vector key, 403 | std::vector iv) { 404 | return DecryptCBC(VectorToArray(in), in.size(), VectorToArray(key), VectorToArray(iv)); 405 | } 406 | 407 | std::vector AES::EncryptCFB(std::vector in, 408 | std::vector key, 409 | std::vector iv) { 410 | return EncryptCFB(VectorToArray(in), in.size(), VectorToArray(key), VectorToArray(iv)); 411 | } 412 | 413 | std::vector AES::DecryptCFB(std::vector in, 414 | std::vector key, 415 | std::vector iv) { 416 | return DecryptCFB(VectorToArray(in), in.size(), VectorToArray(key), VectorToArray(iv)); 417 | } 418 | -------------------------------------------------------------------------------- /app/src/main/cpp/src/AES.h: -------------------------------------------------------------------------------- 1 | #ifndef _AES_H_ 2 | #define _AES_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | enum class AESKeyLength { AES_128, AES_192, AES_256 }; 12 | 13 | class AES { 14 | private: 15 | static constexpr uint32_t Nb = 4; 16 | static constexpr uint32_t blockBytesLen = 4 * Nb * sizeof(uint8_t); 17 | 18 | uint32_t Nk; 19 | uint32_t Nr; 20 | 21 | static void SubBytes(uint8_t state[4][Nb]); 22 | 23 | static void ShiftRow(uint8_t state[4][Nb], size_t i, 24 | size_t n); // shift row i on n positions 25 | 26 | static void ShiftRows(uint8_t state[4][Nb]); 27 | 28 | static uint8_t xtime(uint8_t b); // multiply on x 29 | 30 | static void MixColumns(uint8_t state[4][Nb]); 31 | 32 | static void AddRoundKey(uint8_t state[4][Nb], const uint8_t *key); 33 | 34 | static void SubWord(uint8_t *a); 35 | 36 | static void RotWord(uint8_t *a); 37 | 38 | static void XorWords(const uint8_t *a, const uint8_t *b, uint8_t *c); 39 | 40 | static void Rcon(uint8_t *a, size_t n); 41 | 42 | static void InvSubBytes(uint8_t state[4][Nb]); 43 | 44 | static void InvMixColumns(uint8_t state[4][Nb]); 45 | 46 | static void InvShiftRows(uint8_t state[4][Nb]); 47 | 48 | static void CheckLength(size_t len); 49 | 50 | void KeyExpansion(const uint8_t key[], uint8_t w[]) const; 51 | 52 | void EncryptBlock(const uint8_t in[], uint8_t out[], 53 | uint8_t key[]) const; 54 | 55 | void DecryptBlock(const uint8_t in[], uint8_t out[], 56 | uint8_t key[]) const; 57 | 58 | static void XorBlocks(const uint8_t *a, const uint8_t *b, 59 | uint8_t *c, size_t len); 60 | 61 | static std::vector ArrayToVector(uint8_t *a, size_t len); 62 | 63 | static uint8_t* VectorToArray(std::vector &a); 64 | 65 | public: 66 | explicit AES(const AESKeyLength keyLength = AESKeyLength::AES_256); 67 | 68 | std::vector EncryptECB(const uint8_t in[], size_t inLen, 69 | const uint8_t key[]); 70 | 71 | std::vector DecryptECB(const uint8_t in[], size_t inLen, 72 | const uint8_t key[]); 73 | 74 | std::vector EncryptCBC(const uint8_t in[], size_t inLen, 75 | const uint8_t key[], const uint8_t *iv); 76 | 77 | std::vector DecryptCBC(const uint8_t in[], size_t inLen, 78 | const uint8_t key[], const uint8_t *iv); 79 | 80 | std::vector EncryptCFB(const uint8_t in[], size_t inLen, 81 | const uint8_t key[], const uint8_t *iv); 82 | 83 | std::vector DecryptCFB(const uint8_t in[], size_t inLen, 84 | const uint8_t key[], const uint8_t *iv); 85 | 86 | std::vector EncryptECB(std::vector in, 87 | std::vector key); 88 | 89 | std::vector DecryptECB(std::vector in, 90 | std::vector key); 91 | 92 | std::vector EncryptCBC(std::vector in, 93 | std::vector key, 94 | std::vector iv); 95 | 96 | std::vector DecryptCBC(std::vector in, 97 | std::vector key, 98 | std::vector iv); 99 | 100 | std::vector EncryptCFB(std::vector in, 101 | std::vector key, 102 | std::vector iv); 103 | 104 | std::vector DecryptCFB(std::vector in, 105 | std::vector key, 106 | std::vector iv); 107 | 108 | static void printHexArray(uint8_t a[], size_t n); 109 | 110 | static void printHexVector(const std::vector &a); 111 | }; 112 | 113 | const uint8_t sbox[16][16] = { 114 | { 115 | 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 116 | 0xfe, 0xd7, 0xab, 0x76 117 | }, 118 | { 119 | 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 120 | 0x9c, 0xa4, 0x72, 0xc0 121 | }, 122 | { 123 | 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 124 | 0x71, 0xd8, 0x31, 0x15 125 | }, 126 | { 127 | 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 128 | 0xeb, 0x27, 0xb2, 0x75 129 | }, 130 | { 131 | 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 132 | 0x29, 0xe3, 0x2f, 0x84 133 | }, 134 | { 135 | 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 136 | 0x4a, 0x4c, 0x58, 0xcf 137 | }, 138 | { 139 | 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 140 | 0x50, 0x3c, 0x9f, 0xa8 141 | }, 142 | { 143 | 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 144 | 0x10, 0xff, 0xf3, 0xd2 145 | }, 146 | { 147 | 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 148 | 0x64, 0x5d, 0x19, 0x73 149 | }, 150 | { 151 | 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 152 | 0xde, 0x5e, 0x0b, 0xdb 153 | }, 154 | { 155 | 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 156 | 0x91, 0x95, 0xe4, 0x79 157 | }, 158 | { 159 | 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 160 | 0x65, 0x7a, 0xae, 0x08 161 | }, 162 | { 163 | 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 164 | 0x4b, 0xbd, 0x8b, 0x8a 165 | }, 166 | { 167 | 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 168 | 0x86, 0xc1, 0x1d, 0x9e 169 | }, 170 | { 171 | 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 172 | 0xce, 0x55, 0x28, 0xdf 173 | }, 174 | { 175 | 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 176 | 0xb0, 0x54, 0xbb, 0x16 177 | } 178 | }; 179 | 180 | const uint8_t inv_sbox[16][16] = { 181 | { 182 | 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 183 | 0x81, 0xf3, 0xd7, 0xfb 184 | }, 185 | { 186 | 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 187 | 0xc4, 0xde, 0xe9, 0xcb 188 | }, 189 | { 190 | 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 191 | 0x42, 0xfa, 0xc3, 0x4e 192 | }, 193 | { 194 | 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 195 | 0x6d, 0x8b, 0xd1, 0x25 196 | }, 197 | { 198 | 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 199 | 0x5d, 0x65, 0xb6, 0x92 200 | }, 201 | { 202 | 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 203 | 0xa7, 0x8d, 0x9d, 0x84 204 | }, 205 | { 206 | 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 207 | 0xb8, 0xb3, 0x45, 0x06 208 | }, 209 | { 210 | 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 211 | 0x01, 0x13, 0x8a, 0x6b 212 | }, 213 | { 214 | 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 215 | 0xf0, 0xb4, 0xe6, 0x73 216 | }, 217 | { 218 | 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 219 | 0x1c, 0x75, 0xdf, 0x6e 220 | }, 221 | { 222 | 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 223 | 0xaa, 0x18, 0xbe, 0x1b 224 | }, 225 | { 226 | 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 227 | 0x78, 0xcd, 0x5a, 0xf4 228 | }, 229 | { 230 | 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 231 | 0x27, 0x80, 0xec, 0x5f 232 | }, 233 | { 234 | 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 235 | 0x93, 0xc9, 0x9c, 0xef 236 | }, 237 | { 238 | 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 239 | 0x83, 0x53, 0x99, 0x61 240 | }, 241 | { 242 | 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 243 | 0x55, 0x21, 0x0c, 0x7d 244 | } 245 | }; 246 | 247 | /// Galois Multiplication lookup tables 248 | static const uint8_t GF_MUL_TABLE[15][256] = { 249 | {}, 250 | {}, 251 | 252 | // mul 2 253 | { 254 | 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x10, 0x12, 0x14, 0x16, 255 | 0x18, 0x1a, 0x1c, 0x1e, 0x20, 0x22, 0x24, 0x26, 0x28, 0x2a, 0x2c, 0x2e, 256 | 0x30, 0x32, 0x34, 0x36, 0x38, 0x3a, 0x3c, 0x3e, 0x40, 0x42, 0x44, 0x46, 257 | 0x48, 0x4a, 0x4c, 0x4e, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5a, 0x5c, 0x5e, 258 | 0x60, 0x62, 0x64, 0x66, 0x68, 0x6a, 0x6c, 0x6e, 0x70, 0x72, 0x74, 0x76, 259 | 0x78, 0x7a, 0x7c, 0x7e, 0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c, 0x8e, 260 | 0x90, 0x92, 0x94, 0x96, 0x98, 0x9a, 0x9c, 0x9e, 0xa0, 0xa2, 0xa4, 0xa6, 261 | 0xa8, 0xaa, 0xac, 0xae, 0xb0, 0xb2, 0xb4, 0xb6, 0xb8, 0xba, 0xbc, 0xbe, 262 | 0xc0, 0xc2, 0xc4, 0xc6, 0xc8, 0xca, 0xcc, 0xce, 0xd0, 0xd2, 0xd4, 0xd6, 263 | 0xd8, 0xda, 0xdc, 0xde, 0xe0, 0xe2, 0xe4, 0xe6, 0xe8, 0xea, 0xec, 0xee, 264 | 0xf0, 0xf2, 0xf4, 0xf6, 0xf8, 0xfa, 0xfc, 0xfe, 0x1b, 0x19, 0x1f, 0x1d, 265 | 0x13, 0x11, 0x17, 0x15, 0x0b, 0x09, 0x0f, 0x0d, 0x03, 0x01, 0x07, 0x05, 266 | 0x3b, 0x39, 0x3f, 0x3d, 0x33, 0x31, 0x37, 0x35, 0x2b, 0x29, 0x2f, 0x2d, 267 | 0x23, 0x21, 0x27, 0x25, 0x5b, 0x59, 0x5f, 0x5d, 0x53, 0x51, 0x57, 0x55, 268 | 0x4b, 0x49, 0x4f, 0x4d, 0x43, 0x41, 0x47, 0x45, 0x7b, 0x79, 0x7f, 0x7d, 269 | 0x73, 0x71, 0x77, 0x75, 0x6b, 0x69, 0x6f, 0x6d, 0x63, 0x61, 0x67, 0x65, 270 | 0x9b, 0x99, 0x9f, 0x9d, 0x93, 0x91, 0x97, 0x95, 0x8b, 0x89, 0x8f, 0x8d, 271 | 0x83, 0x81, 0x87, 0x85, 0xbb, 0xb9, 0xbf, 0xbd, 0xb3, 0xb1, 0xb7, 0xb5, 272 | 0xab, 0xa9, 0xaf, 0xad, 0xa3, 0xa1, 0xa7, 0xa5, 0xdb, 0xd9, 0xdf, 0xdd, 273 | 0xd3, 0xd1, 0xd7, 0xd5, 0xcb, 0xc9, 0xcf, 0xcd, 0xc3, 0xc1, 0xc7, 0xc5, 274 | 0xfb, 0xf9, 0xff, 0xfd, 0xf3, 0xf1, 0xf7, 0xf5, 0xeb, 0xe9, 0xef, 0xed, 275 | 0xe3, 0xe1, 0xe7, 0xe5 276 | }, 277 | 278 | // mul 3 279 | { 280 | 0x00, 0x03, 0x06, 0x05, 0x0c, 0x0f, 0x0a, 0x09, 0x18, 0x1b, 0x1e, 0x1d, 281 | 0x14, 0x17, 0x12, 0x11, 0x30, 0x33, 0x36, 0x35, 0x3c, 0x3f, 0x3a, 0x39, 282 | 0x28, 0x2b, 0x2e, 0x2d, 0x24, 0x27, 0x22, 0x21, 0x60, 0x63, 0x66, 0x65, 283 | 0x6c, 0x6f, 0x6a, 0x69, 0x78, 0x7b, 0x7e, 0x7d, 0x74, 0x77, 0x72, 0x71, 284 | 0x50, 0x53, 0x56, 0x55, 0x5c, 0x5f, 0x5a, 0x59, 0x48, 0x4b, 0x4e, 0x4d, 285 | 0x44, 0x47, 0x42, 0x41, 0xc0, 0xc3, 0xc6, 0xc5, 0xcc, 0xcf, 0xca, 0xc9, 286 | 0xd8, 0xdb, 0xde, 0xdd, 0xd4, 0xd7, 0xd2, 0xd1, 0xf0, 0xf3, 0xf6, 0xf5, 287 | 0xfc, 0xff, 0xfa, 0xf9, 0xe8, 0xeb, 0xee, 0xed, 0xe4, 0xe7, 0xe2, 0xe1, 288 | 0xa0, 0xa3, 0xa6, 0xa5, 0xac, 0xaf, 0xaa, 0xa9, 0xb8, 0xbb, 0xbe, 0xbd, 289 | 0xb4, 0xb7, 0xb2, 0xb1, 0x90, 0x93, 0x96, 0x95, 0x9c, 0x9f, 0x9a, 0x99, 290 | 0x88, 0x8b, 0x8e, 0x8d, 0x84, 0x87, 0x82, 0x81, 0x9b, 0x98, 0x9d, 0x9e, 291 | 0x97, 0x94, 0x91, 0x92, 0x83, 0x80, 0x85, 0x86, 0x8f, 0x8c, 0x89, 0x8a, 292 | 0xab, 0xa8, 0xad, 0xae, 0xa7, 0xa4, 0xa1, 0xa2, 0xb3, 0xb0, 0xb5, 0xb6, 293 | 0xbf, 0xbc, 0xb9, 0xba, 0xfb, 0xf8, 0xfd, 0xfe, 0xf7, 0xf4, 0xf1, 0xf2, 294 | 0xe3, 0xe0, 0xe5, 0xe6, 0xef, 0xec, 0xe9, 0xea, 0xcb, 0xc8, 0xcd, 0xce, 295 | 0xc7, 0xc4, 0xc1, 0xc2, 0xd3, 0xd0, 0xd5, 0xd6, 0xdf, 0xdc, 0xd9, 0xda, 296 | 0x5b, 0x58, 0x5d, 0x5e, 0x57, 0x54, 0x51, 0x52, 0x43, 0x40, 0x45, 0x46, 297 | 0x4f, 0x4c, 0x49, 0x4a, 0x6b, 0x68, 0x6d, 0x6e, 0x67, 0x64, 0x61, 0x62, 298 | 0x73, 0x70, 0x75, 0x76, 0x7f, 0x7c, 0x79, 0x7a, 0x3b, 0x38, 0x3d, 0x3e, 299 | 0x37, 0x34, 0x31, 0x32, 0x23, 0x20, 0x25, 0x26, 0x2f, 0x2c, 0x29, 0x2a, 300 | 0x0b, 0x08, 0x0d, 0x0e, 0x07, 0x04, 0x01, 0x02, 0x13, 0x10, 0x15, 0x16, 301 | 0x1f, 0x1c, 0x19, 0x1a 302 | }, 303 | 304 | {}, 305 | {}, 306 | {}, 307 | {}, 308 | {}, 309 | 310 | // mul 9 311 | { 312 | 0x00, 0x09, 0x12, 0x1b, 0x24, 0x2d, 0x36, 0x3f, 0x48, 0x41, 0x5a, 0x53, 313 | 0x6c, 0x65, 0x7e, 0x77, 0x90, 0x99, 0x82, 0x8b, 0xb4, 0xbd, 0xa6, 0xaf, 314 | 0xd8, 0xd1, 0xca, 0xc3, 0xfc, 0xf5, 0xee, 0xe7, 0x3b, 0x32, 0x29, 0x20, 315 | 0x1f, 0x16, 0x0d, 0x04, 0x73, 0x7a, 0x61, 0x68, 0x57, 0x5e, 0x45, 0x4c, 316 | 0xab, 0xa2, 0xb9, 0xb0, 0x8f, 0x86, 0x9d, 0x94, 0xe3, 0xea, 0xf1, 0xf8, 317 | 0xc7, 0xce, 0xd5, 0xdc, 0x76, 0x7f, 0x64, 0x6d, 0x52, 0x5b, 0x40, 0x49, 318 | 0x3e, 0x37, 0x2c, 0x25, 0x1a, 0x13, 0x08, 0x01, 0xe6, 0xef, 0xf4, 0xfd, 319 | 0xc2, 0xcb, 0xd0, 0xd9, 0xae, 0xa7, 0xbc, 0xb5, 0x8a, 0x83, 0x98, 0x91, 320 | 0x4d, 0x44, 0x5f, 0x56, 0x69, 0x60, 0x7b, 0x72, 0x05, 0x0c, 0x17, 0x1e, 321 | 0x21, 0x28, 0x33, 0x3a, 0xdd, 0xd4, 0xcf, 0xc6, 0xf9, 0xf0, 0xeb, 0xe2, 322 | 0x95, 0x9c, 0x87, 0x8e, 0xb1, 0xb8, 0xa3, 0xaa, 0xec, 0xe5, 0xfe, 0xf7, 323 | 0xc8, 0xc1, 0xda, 0xd3, 0xa4, 0xad, 0xb6, 0xbf, 0x80, 0x89, 0x92, 0x9b, 324 | 0x7c, 0x75, 0x6e, 0x67, 0x58, 0x51, 0x4a, 0x43, 0x34, 0x3d, 0x26, 0x2f, 325 | 0x10, 0x19, 0x02, 0x0b, 0xd7, 0xde, 0xc5, 0xcc, 0xf3, 0xfa, 0xe1, 0xe8, 326 | 0x9f, 0x96, 0x8d, 0x84, 0xbb, 0xb2, 0xa9, 0xa0, 0x47, 0x4e, 0x55, 0x5c, 327 | 0x63, 0x6a, 0x71, 0x78, 0x0f, 0x06, 0x1d, 0x14, 0x2b, 0x22, 0x39, 0x30, 328 | 0x9a, 0x93, 0x88, 0x81, 0xbe, 0xb7, 0xac, 0xa5, 0xd2, 0xdb, 0xc0, 0xc9, 329 | 0xf6, 0xff, 0xe4, 0xed, 0x0a, 0x03, 0x18, 0x11, 0x2e, 0x27, 0x3c, 0x35, 330 | 0x42, 0x4b, 0x50, 0x59, 0x66, 0x6f, 0x74, 0x7d, 0xa1, 0xa8, 0xb3, 0xba, 331 | 0x85, 0x8c, 0x97, 0x9e, 0xe9, 0xe0, 0xfb, 0xf2, 0xcd, 0xc4, 0xdf, 0xd6, 332 | 0x31, 0x38, 0x23, 0x2a, 0x15, 0x1c, 0x07, 0x0e, 0x79, 0x70, 0x6b, 0x62, 333 | 0x5d, 0x54, 0x4f, 0x46 334 | }, 335 | 336 | {}, 337 | 338 | // mul 11 339 | { 340 | 0x00, 0x0b, 0x16, 0x1d, 0x2c, 0x27, 0x3a, 0x31, 0x58, 0x53, 0x4e, 0x45, 341 | 0x74, 0x7f, 0x62, 0x69, 0xb0, 0xbb, 0xa6, 0xad, 0x9c, 0x97, 0x8a, 0x81, 342 | 0xe8, 0xe3, 0xfe, 0xf5, 0xc4, 0xcf, 0xd2, 0xd9, 0x7b, 0x70, 0x6d, 0x66, 343 | 0x57, 0x5c, 0x41, 0x4a, 0x23, 0x28, 0x35, 0x3e, 0x0f, 0x04, 0x19, 0x12, 344 | 0xcb, 0xc0, 0xdd, 0xd6, 0xe7, 0xec, 0xf1, 0xfa, 0x93, 0x98, 0x85, 0x8e, 345 | 0xbf, 0xb4, 0xa9, 0xa2, 0xf6, 0xfd, 0xe0, 0xeb, 0xda, 0xd1, 0xcc, 0xc7, 346 | 0xae, 0xa5, 0xb8, 0xb3, 0x82, 0x89, 0x94, 0x9f, 0x46, 0x4d, 0x50, 0x5b, 347 | 0x6a, 0x61, 0x7c, 0x77, 0x1e, 0x15, 0x08, 0x03, 0x32, 0x39, 0x24, 0x2f, 348 | 0x8d, 0x86, 0x9b, 0x90, 0xa1, 0xaa, 0xb7, 0xbc, 0xd5, 0xde, 0xc3, 0xc8, 349 | 0xf9, 0xf2, 0xef, 0xe4, 0x3d, 0x36, 0x2b, 0x20, 0x11, 0x1a, 0x07, 0x0c, 350 | 0x65, 0x6e, 0x73, 0x78, 0x49, 0x42, 0x5f, 0x54, 0xf7, 0xfc, 0xe1, 0xea, 351 | 0xdb, 0xd0, 0xcd, 0xc6, 0xaf, 0xa4, 0xb9, 0xb2, 0x83, 0x88, 0x95, 0x9e, 352 | 0x47, 0x4c, 0x51, 0x5a, 0x6b, 0x60, 0x7d, 0x76, 0x1f, 0x14, 0x09, 0x02, 353 | 0x33, 0x38, 0x25, 0x2e, 0x8c, 0x87, 0x9a, 0x91, 0xa0, 0xab, 0xb6, 0xbd, 354 | 0xd4, 0xdf, 0xc2, 0xc9, 0xf8, 0xf3, 0xee, 0xe5, 0x3c, 0x37, 0x2a, 0x21, 355 | 0x10, 0x1b, 0x06, 0x0d, 0x64, 0x6f, 0x72, 0x79, 0x48, 0x43, 0x5e, 0x55, 356 | 0x01, 0x0a, 0x17, 0x1c, 0x2d, 0x26, 0x3b, 0x30, 0x59, 0x52, 0x4f, 0x44, 357 | 0x75, 0x7e, 0x63, 0x68, 0xb1, 0xba, 0xa7, 0xac, 0x9d, 0x96, 0x8b, 0x80, 358 | 0xe9, 0xe2, 0xff, 0xf4, 0xc5, 0xce, 0xd3, 0xd8, 0x7a, 0x71, 0x6c, 0x67, 359 | 0x56, 0x5d, 0x40, 0x4b, 0x22, 0x29, 0x34, 0x3f, 0x0e, 0x05, 0x18, 0x13, 360 | 0xca, 0xc1, 0xdc, 0xd7, 0xe6, 0xed, 0xf0, 0xfb, 0x92, 0x99, 0x84, 0x8f, 361 | 0xbe, 0xb5, 0xa8, 0xa3 362 | }, 363 | 364 | {}, 365 | 366 | // mul 13 367 | { 368 | 0x00, 0x0d, 0x1a, 0x17, 0x34, 0x39, 0x2e, 0x23, 0x68, 0x65, 0x72, 0x7f, 369 | 0x5c, 0x51, 0x46, 0x4b, 0xd0, 0xdd, 0xca, 0xc7, 0xe4, 0xe9, 0xfe, 0xf3, 370 | 0xb8, 0xb5, 0xa2, 0xaf, 0x8c, 0x81, 0x96, 0x9b, 0xbb, 0xb6, 0xa1, 0xac, 371 | 0x8f, 0x82, 0x95, 0x98, 0xd3, 0xde, 0xc9, 0xc4, 0xe7, 0xea, 0xfd, 0xf0, 372 | 0x6b, 0x66, 0x71, 0x7c, 0x5f, 0x52, 0x45, 0x48, 0x03, 0x0e, 0x19, 0x14, 373 | 0x37, 0x3a, 0x2d, 0x20, 0x6d, 0x60, 0x77, 0x7a, 0x59, 0x54, 0x43, 0x4e, 374 | 0x05, 0x08, 0x1f, 0x12, 0x31, 0x3c, 0x2b, 0x26, 0xbd, 0xb0, 0xa7, 0xaa, 375 | 0x89, 0x84, 0x93, 0x9e, 0xd5, 0xd8, 0xcf, 0xc2, 0xe1, 0xec, 0xfb, 0xf6, 376 | 0xd6, 0xdb, 0xcc, 0xc1, 0xe2, 0xef, 0xf8, 0xf5, 0xbe, 0xb3, 0xa4, 0xa9, 377 | 0x8a, 0x87, 0x90, 0x9d, 0x06, 0x0b, 0x1c, 0x11, 0x32, 0x3f, 0x28, 0x25, 378 | 0x6e, 0x63, 0x74, 0x79, 0x5a, 0x57, 0x40, 0x4d, 0xda, 0xd7, 0xc0, 0xcd, 379 | 0xee, 0xe3, 0xf4, 0xf9, 0xb2, 0xbf, 0xa8, 0xa5, 0x86, 0x8b, 0x9c, 0x91, 380 | 0x0a, 0x07, 0x10, 0x1d, 0x3e, 0x33, 0x24, 0x29, 0x62, 0x6f, 0x78, 0x75, 381 | 0x56, 0x5b, 0x4c, 0x41, 0x61, 0x6c, 0x7b, 0x76, 0x55, 0x58, 0x4f, 0x42, 382 | 0x09, 0x04, 0x13, 0x1e, 0x3d, 0x30, 0x27, 0x2a, 0xb1, 0xbc, 0xab, 0xa6, 383 | 0x85, 0x88, 0x9f, 0x92, 0xd9, 0xd4, 0xc3, 0xce, 0xed, 0xe0, 0xf7, 0xfa, 384 | 0xb7, 0xba, 0xad, 0xa0, 0x83, 0x8e, 0x99, 0x94, 0xdf, 0xd2, 0xc5, 0xc8, 385 | 0xeb, 0xe6, 0xf1, 0xfc, 0x67, 0x6a, 0x7d, 0x70, 0x53, 0x5e, 0x49, 0x44, 386 | 0x0f, 0x02, 0x15, 0x18, 0x3b, 0x36, 0x21, 0x2c, 0x0c, 0x01, 0x16, 0x1b, 387 | 0x38, 0x35, 0x22, 0x2f, 0x64, 0x69, 0x7e, 0x73, 0x50, 0x5d, 0x4a, 0x47, 388 | 0xdc, 0xd1, 0xc6, 0xcb, 0xe8, 0xe5, 0xf2, 0xff, 0xb4, 0xb9, 0xae, 0xa3, 389 | 0x80, 0x8d, 0x9a, 0x97 390 | }, 391 | 392 | // mul 14 393 | { 394 | 0x00, 0x0e, 0x1c, 0x12, 0x38, 0x36, 0x24, 0x2a, 0x70, 0x7e, 0x6c, 0x62, 395 | 0x48, 0x46, 0x54, 0x5a, 0xe0, 0xee, 0xfc, 0xf2, 0xd8, 0xd6, 0xc4, 0xca, 396 | 0x90, 0x9e, 0x8c, 0x82, 0xa8, 0xa6, 0xb4, 0xba, 0xdb, 0xd5, 0xc7, 0xc9, 397 | 0xe3, 0xed, 0xff, 0xf1, 0xab, 0xa5, 0xb7, 0xb9, 0x93, 0x9d, 0x8f, 0x81, 398 | 0x3b, 0x35, 0x27, 0x29, 0x03, 0x0d, 0x1f, 0x11, 0x4b, 0x45, 0x57, 0x59, 399 | 0x73, 0x7d, 0x6f, 0x61, 0xad, 0xa3, 0xb1, 0xbf, 0x95, 0x9b, 0x89, 0x87, 400 | 0xdd, 0xd3, 0xc1, 0xcf, 0xe5, 0xeb, 0xf9, 0xf7, 0x4d, 0x43, 0x51, 0x5f, 401 | 0x75, 0x7b, 0x69, 0x67, 0x3d, 0x33, 0x21, 0x2f, 0x05, 0x0b, 0x19, 0x17, 402 | 0x76, 0x78, 0x6a, 0x64, 0x4e, 0x40, 0x52, 0x5c, 0x06, 0x08, 0x1a, 0x14, 403 | 0x3e, 0x30, 0x22, 0x2c, 0x96, 0x98, 0x8a, 0x84, 0xae, 0xa0, 0xb2, 0xbc, 404 | 0xe6, 0xe8, 0xfa, 0xf4, 0xde, 0xd0, 0xc2, 0xcc, 0x41, 0x4f, 0x5d, 0x53, 405 | 0x79, 0x77, 0x65, 0x6b, 0x31, 0x3f, 0x2d, 0x23, 0x09, 0x07, 0x15, 0x1b, 406 | 0xa1, 0xaf, 0xbd, 0xb3, 0x99, 0x97, 0x85, 0x8b, 0xd1, 0xdf, 0xcd, 0xc3, 407 | 0xe9, 0xe7, 0xf5, 0xfb, 0x9a, 0x94, 0x86, 0x88, 0xa2, 0xac, 0xbe, 0xb0, 408 | 0xea, 0xe4, 0xf6, 0xf8, 0xd2, 0xdc, 0xce, 0xc0, 0x7a, 0x74, 0x66, 0x68, 409 | 0x42, 0x4c, 0x5e, 0x50, 0x0a, 0x04, 0x16, 0x18, 0x32, 0x3c, 0x2e, 0x20, 410 | 0xec, 0xe2, 0xf0, 0xfe, 0xd4, 0xda, 0xc8, 0xc6, 0x9c, 0x92, 0x80, 0x8e, 411 | 0xa4, 0xaa, 0xb8, 0xb6, 0x0c, 0x02, 0x10, 0x1e, 0x34, 0x3a, 0x28, 0x26, 412 | 0x7c, 0x72, 0x60, 0x6e, 0x44, 0x4a, 0x58, 0x56, 0x37, 0x39, 0x2b, 0x25, 413 | 0x0f, 0x01, 0x13, 0x1d, 0x47, 0x49, 0x5b, 0x55, 0x7f, 0x71, 0x63, 0x6d, 414 | 0xd7, 0xd9, 0xcb, 0xc5, 0xef, 0xe1, 0xf3, 0xfd, 0xa7, 0xa9, 0xbb, 0xb5, 415 | 0x9f, 0x91, 0x83, 0x8d 416 | } 417 | }; 418 | 419 | /// circulant MDS matrix 420 | static const uint8_t CMDS[4][4] = { 421 | {2, 3, 1, 1}, {1, 2, 3, 1}, {1, 1, 2, 3}, {3, 1, 1, 2} 422 | }; 423 | 424 | /// Inverse circulant MDS matrix 425 | static const uint8_t INV_CMDS[4][4] = { 426 | {14, 11, 13, 9}, {9, 14, 11, 13}, {13, 9, 14, 11}, {11, 13, 9, 14} 427 | }; 428 | 429 | #endif 430 | -------------------------------------------------------------------------------- /app/src/main/cpp/src/ncm.cpp: -------------------------------------------------------------------------------- 1 | #include "AES.h" 2 | #include "ncm.h" 3 | #include "json.hpp" 4 | #include 5 | #include 6 | 7 | #ifndef READ_DATA_BUFFER_SIZE 8 | #define READ_DATA_BUFFER_SIZE 8192 9 | #endif 10 | 11 | constexpr uint8_t MAGIC_HEADER[] = {0X43, 0X54, 0X45, 0X4E, 0X46, 0X44, 0X41, 0X4D}; 12 | constexpr uint8_t CORE_KEY[] = { 13 | 0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 14 | 0x57 15 | }; 16 | constexpr uint8_t META_KEY[] = { 17 | 0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 18 | 0x28 19 | }; 20 | 21 | std::string base64_decode(const std::string_view in) { 22 | const static uint8_t TABLE[] = { 23 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26 | 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 27 | 63, 52, 53, 54, 55, 56, 57, 58, 28 | 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 29 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 30 | 13, 14, 15, 16, 17, 18, 19, 20, 21, 31 | 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 32 | 27, 28, 29, 30, 31, 32, 33, 34, 35, 33 | 36, 37, 38, 39, 40, 41, 42, 43, 44, 34 | 45, 46, 47, 48, 49, 50, 51 35 | }; 36 | 37 | std::string out; 38 | out.reserve(in.size() * 3 / 4); 39 | 40 | uint32_t buf{}; 41 | int n{}; 42 | for (const auto c: in) { 43 | if (c == '=') { 44 | break; 45 | } 46 | buf = buf << 6 | TABLE[c]; 47 | n += 6; 48 | if (n >= 8) { 49 | n -= 8; 50 | out.push_back(static_cast(buf >> n & 0xFF)); 51 | } 52 | } 53 | return out; 54 | } 55 | 56 | NcmDumpError ncm_dump(std::ifstream &input, std::filesystem::path &outputFolder) { 57 | if (!input) { 58 | return NcmDumpError::InvalidInputStream; 59 | } 60 | if (!exists(outputFolder) && !create_directories(outputFolder)) { 61 | return NcmDumpError::InvalidOutputFolder; 62 | } 63 | 64 | char header[8]{0}; 65 | input.read(header, 8); 66 | if (memcmp(header, MAGIC_HEADER, 8) != 0) { 67 | return NcmDumpError::InvalidNcmFileHeader; 68 | } 69 | 70 | input.seekg(2, std::ios::cur); // skip 2 bytes 71 | 72 | uint8_t lenBuf[4]{0}; 73 | input.read(reinterpret_cast(lenBuf), 4); 74 | uint32_t len = (lenBuf[3] << 8 | lenBuf[2]) << 16 | (lenBuf[1] << 8 | lenBuf[0]); 75 | 76 | memset(lenBuf, 0, sizeof(lenBuf)); // reset buffer 77 | 78 | std::vector _key(len); 79 | input.read(reinterpret_cast(_key.data()), len); 80 | for (size_t i{}; i < len; ++i) { 81 | _key[i] ^= 0x64; 82 | } 83 | 84 | // AES-ECB Pkcs7padding 85 | AES aes(AESKeyLength::AES_128); 86 | 87 | auto keyBox(aes.DecryptECB(_key.data(), _key.size(), CORE_KEY)); 88 | keyBox.erase(keyBox.begin(), keyBox.begin() + 17); // substring `neteasecloudmusic` 89 | keyBox.resize(256); 90 | std::fill(std::find(keyBox.begin(), keyBox.end(), '\r'), keyBox.end(), 0); // fill 0 91 | { // RC4 init 92 | size_t keylen = strlen(reinterpret_cast(keyBox.data())); 93 | uint8_t ch[256]{0}; 94 | uint8_t j = 0; 95 | for (size_t i{}; i < 256; ++i) { 96 | ch[i] = static_cast(i); 97 | } 98 | for (size_t i{}; i < 256; ++i) { 99 | j = j + ch[i] + keyBox[i % keylen] & 0xFF; 100 | std::swap(ch[i], ch[j]); 101 | } 102 | keyBox = std::vector(ch, ch + 256); 103 | } 104 | 105 | // read music info 106 | input.read(reinterpret_cast(lenBuf), 4); 107 | len = (lenBuf[3] << 8 | lenBuf[2]) << 16 | (lenBuf[1] << 8 | lenBuf[0]); 108 | memset(lenBuf, 0, sizeof(lenBuf)); // reset buffer 109 | 110 | std::vector metadata(len); 111 | input.read(reinterpret_cast(metadata.data()), len); 112 | if (!input) { 113 | return NcmDumpError::CannotReadMusicInfo; 114 | } 115 | 116 | for (size_t i{}; i < len; ++i) { 117 | metadata[i] ^= 0x63; 118 | } 119 | std::string meta(metadata.begin(), metadata.end()); 120 | 121 | // decrypt music info 122 | meta = meta.substr(22); // substring `163 key(Don't modify):` 123 | const std::string decBase64 = base64_decode(meta); 124 | const auto decMeta(aes.DecryptECB(reinterpret_cast(decBase64.c_str()), 125 | decBase64.length(), 126 | META_KEY)); 127 | 128 | std::string metaJsonString(decMeta.begin(), decMeta.end()); 129 | metaJsonString = metaJsonString.substr(6); // substring `music:` 130 | metaJsonString.erase(metaJsonString.find_last_of('}') + 1); // fix '\a' at the end of string 131 | auto metaJson = nlohmann::json::parse(metaJsonString); 132 | std::string fileFormat = metaJson["format"]; 133 | std::transform(fileFormat.begin(), fileFormat.end(), fileFormat.begin(), 134 | [](const char c) { return std::tolower(c); }); 135 | // make file format to lower case 136 | if (fileFormat.empty()) { 137 | return NcmDumpError::UnknownFileFormat; 138 | } 139 | 140 | // read music cover data 141 | input.seekg(9, std::ios::cur); // seek crc & gap 142 | if (!input) { 143 | return NcmDumpError::CannotReadMusicCover; 144 | } 145 | 146 | input.read(reinterpret_cast(lenBuf), 4); 147 | len = (lenBuf[3] << 8 | lenBuf[2]) << 16 | (lenBuf[1] << 8 | lenBuf[0]); 148 | memset(lenBuf, 0, sizeof(lenBuf)); // reset buffer 149 | // std::vector coverData(len); 150 | // input.read(reinterpret_cast(coverData.data()), len); 151 | input.seekg(len, std::ios::cur); // skip cover data 152 | 153 | std::string musicName = metaJson["musicName"]; 154 | std::string album = metaJson["album"]; 155 | std::vector> artist = metaJson["artist"]; // [["Name", uid], ...] 156 | // uint32_t bitrate = metaJson["bitrate"]; 157 | // uint64_t duration = metaJson["duration"]; 158 | 159 | std::string artists; 160 | for (auto &v: artist) { 161 | for (auto &jobj: v) { 162 | if (jobj.is_string()) { 163 | artists.append(jobj.get()); 164 | artists.append(" "); 165 | } 166 | } 167 | } 168 | // trim end 169 | if (artists.ends_with(' ')) 170 | artists.erase(artists.end() - 1); 171 | 172 | // combine file name 173 | std::string fileName = artists + " - " + musicName + "." + fileFormat; 174 | std::ofstream output(outputFolder / fileName, std::ios::binary | std::ios::ate); 175 | 176 | // check stream valid 177 | if (!output) { 178 | return NcmDumpError::CannotSaveOutputFile; 179 | } 180 | if (!input) { 181 | return NcmDumpError::CannotReadMusicData; 182 | } 183 | 184 | // process music data 185 | auto buf = std::make_unique(READ_DATA_BUFFER_SIZE); 186 | // decode data 187 | while (!input.eof()) { 188 | input.read(reinterpret_cast(buf.get()), READ_DATA_BUFFER_SIZE); 189 | std::streamsize cnt = input.gcount(); 190 | 191 | for (size_t i{}; i < cnt; ++i) { 192 | uint8_t j = (i + 1) & 0xff; 193 | buf[i] ^= keyBox[(keyBox[j] + keyBox[(keyBox[j] + j) & 0xff]) & 0xff]; 194 | } 195 | 196 | output.write(reinterpret_cast(buf.get()), cnt); 197 | if (!output) { 198 | return NcmDumpError::CannotSaveOutputFile; 199 | } 200 | } 201 | 202 | output.flush(); 203 | output.close(); 204 | 205 | return NcmDumpError::Success; 206 | } 207 | -------------------------------------------------------------------------------- /app/src/main/cpp/src/ncm.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | enum class NcmDumpError : uint8_t { 6 | Success = 0, 7 | InvalidInputStream, 8 | InvalidOutputFolder, 9 | InvalidNcmFileHeader, 10 | UnknownFileFormat, 11 | CannotReadMusicInfo, 12 | CannotReadMusicCover, 13 | CannotReadMusicData, 14 | CannotSaveOutputFile, 15 | }; 16 | 17 | [[nodiscard]] NcmDumpError ncm_dump(std::ifstream &input, std::filesystem::path &outputFolder); 18 | -------------------------------------------------------------------------------- /app/src/main/cpp/src/ncmdumper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "ncm.h" 5 | 6 | extern "C" { 7 | JNIEXPORT jint JNICALL 8 | Java_me_kyuubiran_ncmdumper_ui_utils_Dumper_dumpNcmFile(JNIEnv *env, jclass clazz, 9 | jstring in_file_path, 10 | jstring out_file_path) { 11 | 12 | std::string inPath; 13 | std::string outPath; 14 | 15 | jboolean isCopy = false; 16 | 17 | inPath = env->GetStringUTFChars(in_file_path, &isCopy); 18 | outPath = env->GetStringUTFChars(out_file_path, &isCopy); 19 | 20 | std::ifstream ifs(inPath, std::ios::in | std::ios::binary); 21 | std::filesystem::path out(outPath); 22 | NcmDumpError err = ncm_dump(ifs, out); 23 | 24 | return static_cast(err); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/ncmdumper/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.ncmdumper 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.content.SharedPreferences 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.os.Environment 9 | import android.provider.Settings 10 | import androidx.activity.ComponentActivity 11 | import androidx.activity.compose.setContent 12 | import androidx.activity.enableEdgeToEdge 13 | import androidx.compose.foundation.layout.Box 14 | import androidx.compose.foundation.layout.safeDrawingPadding 15 | import androidx.compose.foundation.layout.statusBarsPadding 16 | import androidx.compose.material.AlertDialog 17 | import androidx.compose.material.Text 18 | import androidx.compose.material.TextButton 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.DisposableEffect 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.platform.LocalLifecycleOwner 27 | import androidx.compose.ui.res.stringResource 28 | import androidx.lifecycle.DefaultLifecycleObserver 29 | import androidx.lifecycle.Lifecycle 30 | import androidx.lifecycle.LifecycleEventObserver 31 | import androidx.lifecycle.LifecycleOwner 32 | import androidx.navigation.compose.NavHost 33 | import androidx.navigation.compose.composable 34 | import androidx.navigation.compose.rememberNavController 35 | import me.kyuubiran.ncmdumper.ui.pages.MainPage 36 | import me.kyuubiran.ncmdumper.ui.pages.SettingsPage 37 | import me.kyuubiran.ncmdumper.ui.theme.MyTheme 38 | 39 | class MainActivity : ComponentActivity() { 40 | 41 | companion object { 42 | lateinit var sp: SharedPreferences 43 | private set 44 | } 45 | 46 | override fun onCreate(savedInstanceState: Bundle?) { 47 | super.onCreate(savedInstanceState) 48 | sp = getSharedPreferences("settings", Activity.MODE_PRIVATE) 49 | 50 | enableEdgeToEdge() 51 | setContent { 52 | val navController = rememberNavController() 53 | MyTheme { 54 | RequireAllFileAccessPermissionDialog(this) 55 | Box(modifier = Modifier.safeDrawingPadding()) { 56 | NavHost(navController = navController, startDestination = "main_page") { 57 | composable("main_page") { MainPage.View(navController, this@MainActivity) } 58 | composable("settings_page") { SettingsPage.View(navController, this@MainActivity) } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | @Composable 67 | private fun RequireAllFileAccessPermissionDialog(activity: Activity) { 68 | if (Build.VERSION.SDK_INT < 30) 69 | return 70 | 71 | var showed by remember { mutableStateOf(!Environment.isExternalStorageManager()) } 72 | 73 | val lifecycleOwner = LocalLifecycleOwner.current 74 | DisposableEffect(lifecycleOwner) { 75 | val lifecycle = LifecycleEventObserver { _, e -> 76 | if (e != Lifecycle.Event.ON_RESUME) 77 | return@LifecycleEventObserver 78 | 79 | if (Build.VERSION.SDK_INT < 30) 80 | return@LifecycleEventObserver 81 | 82 | if (showed) { 83 | showed = !Environment.isExternalStorageManager() 84 | } 85 | } 86 | 87 | lifecycleOwner.lifecycle.addObserver(lifecycle) 88 | onDispose { lifecycleOwner.lifecycle.removeObserver(lifecycle) } 89 | } 90 | 91 | if (showed) { 92 | AlertDialog( 93 | onDismissRequest = { }, 94 | confirmButton = { 95 | TextButton( 96 | onClick = { 97 | val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) 98 | activity.startActivity(intent) 99 | }, 100 | ) { Text(text = stringResource(id = R.string.confirm)) } 101 | }, 102 | title = { Text(text = stringResource(id = R.string.title_dialog_require_all_file_access_permission)) }, 103 | text = { Text(text = stringResource(id = R.string.message_dialog_require_all_file_access_permission)) }, 104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/ncmdumper/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.ncmdumper 2 | 3 | import android.app.Application 4 | 5 | class MyApplication : Application() { 6 | override fun onCreate() { 7 | super.onCreate() 8 | System.loadLibrary("ncmdumper") 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/ncmdumper/ui/pages/MainPage.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.ncmdumper.ui.pages 2 | 3 | import android.os.Environment 4 | import android.util.Log 5 | import androidx.activity.compose.BackHandler 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.isSystemInDarkTheme 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.Row 13 | import androidx.compose.foundation.layout.fillMaxSize 14 | import androidx.compose.foundation.layout.fillMaxWidth 15 | import androidx.compose.foundation.layout.height 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.layout.statusBarsPadding 19 | import androidx.compose.foundation.lazy.LazyColumn 20 | import androidx.compose.foundation.lazy.items 21 | import androidx.compose.foundation.shape.CircleShape 22 | import androidx.compose.foundation.text.selection.LocalTextSelectionColors 23 | import androidx.compose.foundation.text.selection.SelectionContainer 24 | import androidx.compose.foundation.text.selection.TextSelectionColors 25 | import androidx.compose.material.DropdownMenu 26 | import androidx.compose.material.DropdownMenuItem 27 | import androidx.compose.material.ExperimentalMaterialApi 28 | import androidx.compose.material.OutlinedTextField 29 | import androidx.compose.material.Text 30 | import androidx.compose.material.TextField 31 | import androidx.compose.material.TextFieldColors 32 | import androidx.compose.material.TextFieldDefaults 33 | import androidx.compose.material.TopAppBar 34 | import androidx.compose.material.pullrefresh.PullRefreshIndicator 35 | import androidx.compose.material.pullrefresh.pullRefresh 36 | import androidx.compose.material.pullrefresh.rememberPullRefreshState 37 | import androidx.compose.runtime.Composable 38 | import androidx.compose.runtime.CompositionLocalProvider 39 | import androidx.compose.runtime.LaunchedEffect 40 | import androidx.compose.runtime.getValue 41 | import androidx.compose.runtime.mutableStateListOf 42 | import androidx.compose.runtime.mutableStateOf 43 | import androidx.compose.runtime.remember 44 | import androidx.compose.runtime.rememberCoroutineScope 45 | import androidx.compose.runtime.setValue 46 | import androidx.compose.ui.Alignment 47 | import androidx.compose.ui.Modifier 48 | import androidx.compose.ui.draw.clip 49 | import androidx.compose.ui.graphics.Color 50 | import androidx.compose.ui.graphics.ColorFilter 51 | import androidx.compose.ui.res.painterResource 52 | import androidx.compose.ui.res.stringResource 53 | import androidx.compose.ui.unit.dp 54 | import androidx.navigation.NavHostController 55 | import kotlinx.coroutines.Dispatchers 56 | import kotlinx.coroutines.delay 57 | import kotlinx.coroutines.launch 58 | import kotlinx.coroutines.withContext 59 | import me.kyuubiran.ncmdumper.MainActivity 60 | import me.kyuubiran.ncmdumper.R 61 | import me.kyuubiran.ncmdumper.ui.theme.BackGroundColor 62 | import me.kyuubiran.ncmdumper.ui.theme.LightBlue 63 | import me.kyuubiran.ncmdumper.ui.theme.LightDark 64 | import me.kyuubiran.ncmdumper.ui.utils.Dumper 65 | import me.kyuubiran.ncmdumper.ui.views.NcmFileInfo 66 | import me.kyuubiran.ncmdumper.ui.views.NcmFileItem 67 | import java.io.File 68 | 69 | object MainPage { 70 | private val list = mutableStateListOf() 71 | private val listFiltered = mutableStateListOf() 72 | 73 | private fun reloadFiles() { 74 | val f = File(Dumper.DEFAULT_NETEASE_MUSIC_PATH) 75 | list.clear() 76 | if (!f.exists() || !f.isDirectory) 77 | return 78 | 79 | list += f.listFiles { ff -> ff.extension.lowercase() == "ncm" }?.toList() ?: emptyList() 80 | 81 | Log.i("MainFragment", "Found ${list.size} ncm files") 82 | } 83 | 84 | private fun reloadFilesFullDisk() { 85 | val f = Environment.getExternalStorageDirectory() 86 | 87 | list.clear() 88 | 89 | fun doSearch(f: File) { 90 | if (!f.exists()) 91 | return 92 | 93 | if (f.isDirectory) { 94 | f.listFiles()?.forEach { 95 | doSearch(it) 96 | } 97 | } else { 98 | if (f.extension.lowercase() == "ncm") 99 | list += f 100 | } 101 | } 102 | 103 | doSearch(f) 104 | } 105 | 106 | private fun updateSearch(str: String) { 107 | if (!showSearch.value) 108 | return 109 | 110 | listFiltered.clear() 111 | if (str.isBlank()) { 112 | listFiltered += list 113 | return 114 | } 115 | 116 | val args = str.split(' ') 117 | list.forEach { 118 | listFiltered.apply { 119 | val name = it.name 120 | if (args.all { arg -> name.contains(arg, ignoreCase = true) }) 121 | add(it) 122 | } 123 | } 124 | } 125 | 126 | @Composable 127 | private fun AppBar(controller: NavHostController) { 128 | var moreShowed by remember { mutableStateOf(false) } 129 | TopAppBar( 130 | title = { 131 | if (!showSearch.value) { 132 | Text(text = stringResource(id = R.string.app_name)) 133 | } else { 134 | Row( 135 | modifier = Modifier.fillMaxWidth(), 136 | verticalAlignment = Alignment.CenterVertically 137 | ) { 138 | Image( 139 | modifier = Modifier 140 | .clip(CircleShape) 141 | .clickable { showSearch.value = false }, 142 | painter = painterResource(id = R.drawable.baseline_arrow_back_24), 143 | contentDescription = stringResource(id = R.string.back), 144 | colorFilter = ColorFilter.tint(Color.White) 145 | ) 146 | 147 | var text by remember { mutableStateOf("") } 148 | val selection = TextSelectionColors( 149 | handleColor = LightBlue, 150 | backgroundColor = Color.DarkGray, 151 | ) 152 | CompositionLocalProvider(LocalTextSelectionColors provides selection) { 153 | SelectionContainer { 154 | OutlinedTextField( 155 | modifier = Modifier 156 | .fillMaxWidth() 157 | .padding(start = 8.dp, end = 8.dp), 158 | value = text, 159 | onValueChange = { 160 | text = it 161 | updateSearch(it) 162 | }, 163 | placeholder = { 164 | Text(text = stringResource(id = R.string.search_with_ellipsis), color = Color.Gray) 165 | }, 166 | singleLine = true, 167 | colors = TextFieldDefaults.outlinedTextFieldColors( 168 | unfocusedBorderColor = Color.Transparent, 169 | focusedBorderColor = Color.Transparent, 170 | cursorColor = Color.White) 171 | ) 172 | } 173 | } 174 | } 175 | } 176 | }, 177 | actions = { 178 | if (!showSearch.value) 179 | Box( 180 | modifier = Modifier 181 | .padding(end = 8.dp) 182 | .size(36.dp) 183 | .clip(CircleShape) 184 | .clickable { 185 | moreShowed = !moreShowed 186 | }, 187 | contentAlignment = Alignment.Center 188 | ) { 189 | Column { 190 | Image( 191 | modifier = Modifier.size(28.dp), 192 | painter = painterResource(id = R.drawable.baseline_more_vert_24), 193 | contentDescription = stringResource(id = R.string.settings), 194 | colorFilter = ColorFilter.tint(Color.White) 195 | ) 196 | DropdownMenu( 197 | expanded = moreShowed, 198 | onDismissRequest = { moreShowed = false }, 199 | ) { 200 | DropdownMenuItem( 201 | onClick = { 202 | showSearch.value = true 203 | moreShowed = false 204 | } 205 | ) { 206 | Text(text = stringResource(id = R.string.search)) 207 | } 208 | DropdownMenuItem( 209 | onClick = { 210 | controller.navigate("settings_page") 211 | moreShowed = false 212 | } 213 | ) { 214 | Text(text = stringResource(id = R.string.settings)) 215 | } 216 | } 217 | } 218 | } 219 | }, 220 | ) 221 | } 222 | 223 | @OptIn(ExperimentalMaterialApi::class) 224 | @Composable 225 | private fun NcmFileList() { 226 | val sp = MainActivity.sp 227 | 228 | val refreshScope = rememberCoroutineScope() 229 | var refreshing by remember { mutableStateOf(false) } 230 | 231 | fun refresh() = refreshScope.launch { 232 | refreshing = true 233 | val beg = System.currentTimeMillis() 234 | withContext(Dispatchers.IO) { 235 | if (sp.getBoolean("search_full_disk", false)) 236 | reloadFilesFullDisk() 237 | else 238 | reloadFiles() 239 | 240 | listFiltered.clear() 241 | listFiltered += list 242 | } 243 | val end = System.currentTimeMillis() 244 | if (end - beg < 500) 245 | delay(500 - (end - beg)) 246 | 247 | refreshing = false 248 | } 249 | 250 | LaunchedEffect("refresh") { 251 | refresh() 252 | } 253 | 254 | val state = rememberPullRefreshState(refreshing, ::refresh) 255 | Box( 256 | modifier = Modifier 257 | .pullRefresh(state) 258 | .background(BackGroundColor) 259 | ) { 260 | val showList = if (showSearch.value) 261 | listFiltered 262 | else 263 | list 264 | 265 | LazyColumn(modifier = Modifier.fillMaxSize()) { 266 | items(showList) { file -> 267 | NcmFileItem(NcmFileInfo(file)) 268 | } 269 | } 270 | 271 | if (showList.isEmpty()) { 272 | Text( 273 | text = stringResource(id = R.string.nothing_here), 274 | modifier = Modifier.align(Alignment.Center) 275 | ) 276 | } 277 | 278 | PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) 279 | } 280 | } 281 | 282 | private val showSearch = mutableStateOf(false) 283 | 284 | @Composable 285 | fun View(controller: NavHostController, activity: MainActivity) { 286 | BackHandler(enabled = showSearch.value) { 287 | showSearch.value = false 288 | } 289 | 290 | Column { 291 | AppBar(controller) 292 | NcmFileList() 293 | } 294 | } 295 | } 296 | 297 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/ncmdumper/ui/pages/SettingsPage.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.ncmdumper.ui.pages 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Environment 7 | import android.util.Log 8 | import androidx.activity.compose.rememberLauncherForActivityResult 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.compose.foundation.Image 11 | import androidx.compose.foundation.background 12 | import androidx.compose.foundation.clickable 13 | import androidx.compose.foundation.gestures.Orientation 14 | import androidx.compose.foundation.gestures.scrollable 15 | import androidx.compose.foundation.isSystemInDarkTheme 16 | import androidx.compose.foundation.layout.Arrangement 17 | import androidx.compose.foundation.layout.Box 18 | import androidx.compose.foundation.layout.Column 19 | import androidx.compose.foundation.layout.Row 20 | import androidx.compose.foundation.layout.defaultMinSize 21 | import androidx.compose.foundation.layout.fillMaxSize 22 | import androidx.compose.foundation.layout.fillMaxWidth 23 | import androidx.compose.foundation.layout.padding 24 | import androidx.compose.foundation.layout.statusBarsPadding 25 | import androidx.compose.foundation.rememberScrollState 26 | import androidx.compose.foundation.shape.CircleShape 27 | import androidx.compose.material.MaterialTheme 28 | import androidx.compose.material.Switch 29 | import androidx.compose.material.Text 30 | import androidx.compose.material.TopAppBar 31 | import androidx.compose.runtime.Composable 32 | import androidx.compose.runtime.getValue 33 | import androidx.compose.runtime.mutableStateOf 34 | import androidx.compose.runtime.remember 35 | import androidx.compose.runtime.setValue 36 | import androidx.compose.ui.Alignment 37 | import androidx.compose.ui.Modifier 38 | import androidx.compose.ui.draw.clip 39 | import androidx.compose.ui.graphics.Color 40 | import androidx.compose.ui.graphics.ColorFilter 41 | import androidx.compose.ui.res.painterResource 42 | import androidx.compose.ui.res.stringResource 43 | import androidx.compose.ui.tooling.preview.Preview 44 | import androidx.compose.ui.unit.dp 45 | import androidx.compose.ui.unit.sp 46 | import androidx.navigation.NavHostController 47 | import me.kyuubiran.ncmdumper.BuildConfig 48 | import me.kyuubiran.ncmdumper.MainActivity 49 | import me.kyuubiran.ncmdumper.R 50 | import me.kyuubiran.ncmdumper.ui.theme.BackGroundColor 51 | import me.kyuubiran.ncmdumper.ui.utils.Dumper 52 | 53 | object SettingsPage { 54 | @Composable 55 | private fun AppBar(navController: NavHostController) { 56 | TopAppBar( 57 | title = { 58 | Row { 59 | Image( 60 | modifier = Modifier 61 | .clip(CircleShape) 62 | .clickable { navController.popBackStack() }, 63 | painter = painterResource(id = R.drawable.baseline_arrow_back_24), 64 | contentDescription = stringResource(id = R.string.back), 65 | colorFilter = ColorFilter.tint(Color.White) 66 | ) 67 | Text(modifier = Modifier.padding(start = 8.dp), text = stringResource(id = R.string.settings)) 68 | } 69 | }, 70 | ) 71 | } 72 | 73 | @Composable 74 | private fun Configs(activity: MainActivity) { 75 | val sp = MainActivity.sp 76 | var outputFolder by remember { mutableStateOf(sp.getString("output_path", Dumper.DEFAULT_NETEASE_MUSIC_PATH)!!) } 77 | 78 | val folderPicker = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { 79 | if (it.resultCode == Activity.RESULT_OK) { 80 | val uri = it.data?.data?.path 81 | if (uri != null) { 82 | val realPath = Environment.getExternalStorageDirectory().path + "/" + 83 | uri.substring(uri.indexOf(':') + 1) 84 | 85 | Log.i("SettingsPage", "Set output path: $realPath") 86 | sp.edit().putString("output_path", realPath).apply() 87 | outputFolder = realPath 88 | } 89 | } 90 | } 91 | 92 | 93 | val scrollable = rememberScrollState() 94 | Column( 95 | modifier = Modifier 96 | .fillMaxSize() 97 | .scrollable(state = scrollable, orientation = Orientation.Horizontal) 98 | .background(BackGroundColor) 99 | ) { 100 | // region Dumper 101 | Text( 102 | modifier = Modifier 103 | .padding(start = 8.dp, top = 16.dp, bottom = 4.dp), 104 | text = stringResource(id = R.string.title_dumper), 105 | color = MaterialTheme.colors.primary, 106 | fontSize = 18.sp 107 | ) 108 | 109 | var searchFullDiskEnabled by remember { mutableStateOf(sp.getBoolean("search_full_disk", false)) } 110 | SwitchConfigItem( 111 | title = stringResource(id = R.string.title_config_search_full_disk), 112 | subtitle = stringResource(id = R.string.subtitle_config_search_full_disk), 113 | isChecked = searchFullDiskEnabled, 114 | onChanged = { 115 | searchFullDiskEnabled = it 116 | sp.edit().putBoolean("search_full_disk", it).apply() 117 | } 118 | ) 119 | 120 | BaseConfigItem( 121 | title = stringResource(id = R.string.title_config_set_output_folder), 122 | subtitle = outputFolder, 123 | onClick = { 124 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) 125 | intent.addCategory(Intent.CATEGORY_DEFAULT) 126 | folderPicker.launch(intent) 127 | } 128 | ) 129 | // endregion 130 | 131 | // region about 132 | Text( 133 | modifier = Modifier 134 | .padding(start = 8.dp, top = 16.dp, bottom = 4.dp), 135 | text = stringResource(id = R.string.title_about), 136 | color = MaterialTheme.colors.primary, 137 | fontSize = 18.sp 138 | ) 139 | BaseConfigItem( 140 | title = stringResource(id = R.string.title_version), 141 | subtitle = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", 142 | ) 143 | BaseConfigItem( 144 | title = stringResource(id = R.string.title_config_goto_github_page), 145 | subtitle = stringResource(id = R.string.subtitle_config_goto_github_page), 146 | onClick = { 147 | val intent = Intent(Intent.ACTION_VIEW) 148 | intent.data = Uri.parse("https://github.com/KyuubiRan/NcmDumperApp") 149 | activity.startActivity(intent) 150 | } 151 | ) 152 | // endregion 153 | } 154 | } 155 | 156 | @Composable 157 | fun View(navController: NavHostController, activity: MainActivity) { 158 | Column { 159 | AppBar(navController = navController) 160 | Configs(activity = activity) 161 | } 162 | } 163 | } 164 | 165 | @Composable 166 | private fun BaseConfigItem( 167 | title: String, 168 | subtitle: String? = null, 169 | onClick: (() -> Unit) = {}, 170 | rightView: @Composable () -> Unit = { 171 | Text(text = ">", fontSize = 20.sp, color = Color.Gray) 172 | } 173 | ) { 174 | Box( 175 | modifier = Modifier 176 | .clickable(onClick = onClick) 177 | ) { 178 | Row( 179 | modifier = Modifier.fillMaxWidth() 180 | ) { 181 | Column( 182 | modifier = Modifier 183 | .defaultMinSize(minHeight = 60.dp) 184 | .fillMaxWidth(.8f) 185 | .padding(8.dp), 186 | verticalArrangement = Arrangement.Center 187 | ) { 188 | Text(text = title, fontSize = 18.sp, color = if (isSystemInDarkTheme()) Color.White else Color.Black) 189 | if (!subtitle.isNullOrBlank()) 190 | Text(text = subtitle, fontSize = 14.sp, color = Color.Gray) 191 | } 192 | 193 | Box( 194 | modifier = Modifier 195 | .fillMaxWidth() 196 | .align(alignment = Alignment.CenterVertically) 197 | .padding(start = 8.dp), 198 | contentAlignment = Alignment.Center 199 | ) { 200 | rightView() 201 | } 202 | } 203 | } 204 | } 205 | 206 | @Composable 207 | private fun SwitchConfigItem( 208 | title: String, 209 | subtitle: String? = null, 210 | isChecked: Boolean, 211 | onChanged: (Boolean) -> Unit = {}, 212 | ) { 213 | BaseConfigItem( 214 | title = title, 215 | subtitle = subtitle, 216 | onClick = { onChanged(!isChecked) }, 217 | rightView = { 218 | Switch(checked = isChecked, onCheckedChange = { onChanged(!isChecked) }) 219 | } 220 | ) 221 | } 222 | 223 | @Preview 224 | @Composable 225 | private fun TestBaseConfigItem() { 226 | Column { 227 | BaseConfigItem(title = "标题", subtitle = "测试", onClick = { }) 228 | SwitchConfigItem(title = "测试2", isChecked = true, onChanged = { }) 229 | } 230 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/ncmdumper/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.ncmdumper.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | 7 | val Purple80 = Color(0xFFD0BCFF) 8 | val PurpleGrey80 = Color(0xFFCCC2DC) 9 | val Pink80 = Color(0xFFEFB8C8) 10 | 11 | val Purple40 = Color(0xFF6650a4) 12 | val PurpleGrey40 = Color(0xFF625b71) 13 | val Pink40 = Color(0xFF7D5260) 14 | val LightDark = Color(0xFF1E1E1E) 15 | val LightBlue = Color(0xFF1E88E5) 16 | 17 | val BackGroundColor 18 | @Composable get() = if (isSystemInDarkTheme()) LightDark else Color.White 19 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/ncmdumper/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.ncmdumper.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.Colors 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.graphics.Color.Companion.Black 8 | import androidx.compose.ui.graphics.Color.Companion.Red 9 | import androidx.compose.ui.graphics.Color.Companion.White 10 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 11 | 12 | private val DarkColorScheme = Colors( 13 | primary = Pink40, 14 | primaryVariant = Pink80, 15 | secondary = PurpleGrey80, 16 | secondaryVariant = PurpleGrey80, 17 | background = Black, 18 | surface = Black, 19 | error = Red, 20 | onPrimary = White, 21 | onSecondary = White, 22 | onBackground = White, 23 | onSurface = White, 24 | onError = White, 25 | isLight = false, 26 | ) 27 | 28 | private val LightColorScheme = Colors( 29 | primary = Pink40, 30 | primaryVariant = Pink80, 31 | secondary = PurpleGrey80, 32 | secondaryVariant = PurpleGrey80, 33 | background = White, 34 | surface = White, 35 | error = Red, 36 | onPrimary = White, 37 | onSecondary = White, 38 | onBackground = Black, 39 | onSurface = Black, 40 | onError = White, 41 | isLight = true 42 | ) 43 | 44 | @Composable 45 | fun MyTheme( 46 | darkTheme: Boolean = isSystemInDarkTheme(), 47 | // Dynamic color is available on Android 12+ 48 | dynamicColor: Boolean = true, 49 | content: @Composable () -> Unit 50 | ) { 51 | val colorScheme = when { 52 | // dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 53 | // val context = LocalContext.current 54 | // if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 55 | // } 56 | 57 | darkTheme -> DarkColorScheme 58 | else -> LightColorScheme 59 | } 60 | 61 | 62 | val systemUiController = rememberSystemUiController() 63 | systemUiController.setSystemBarsColor( 64 | color = if (darkTheme) colorScheme.surface else colorScheme.primary, 65 | ) 66 | 67 | MaterialTheme( 68 | colors = colorScheme, 69 | typography = Typography, 70 | content = content 71 | ) 72 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/ncmdumper/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.ncmdumper.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 14.sp, 15 | lineHeight = 20.sp, 16 | letterSpacing = 0.sp 17 | ), 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/ncmdumper/ui/utils/DpUtils.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.ncmdumper.ui.utils 2 | 3 | import android.content.res.Resources 4 | 5 | private val density = Resources.getSystem().displayMetrics.density 6 | 7 | val Float.px2dp: Float 8 | get() = this / density -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/ncmdumper/ui/utils/Dumper.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.ncmdumper.ui.utils 2 | 3 | object Dumper { 4 | @JvmStatic 5 | external fun dumpNcmFile(inFilePath: String, outFilePath: String): Int 6 | 7 | const val DEFAULT_NETEASE_MUSIC_PATH = "/storage/emulated/0/netease/cloudmusic/Music" 8 | } 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/ncmdumper/ui/views/NcmFileItem.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.ncmdumper.ui.views 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import androidx.compose.foundation.ExperimentalFoundationApi 6 | import androidx.compose.foundation.basicMarquee 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.Column 11 | import androidx.compose.foundation.layout.Row 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.material.AlertDialog 15 | import androidx.compose.material.Card 16 | import androidx.compose.material.CircularProgressIndicator 17 | import androidx.compose.material.Text 18 | import androidx.compose.material.TextButton 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableIntStateOf 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.rememberCoroutineScope 25 | import androidx.compose.runtime.setValue 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.graphics.Color 29 | import androidx.compose.ui.platform.LocalContext 30 | import androidx.compose.ui.res.stringResource 31 | import androidx.compose.ui.unit.dp 32 | import androidx.compose.ui.unit.sp 33 | import kotlinx.coroutines.Dispatchers 34 | import kotlinx.coroutines.delay 35 | import kotlinx.coroutines.launch 36 | import kotlinx.coroutines.withContext 37 | import me.kyuubiran.ncmdumper.R 38 | import me.kyuubiran.ncmdumper.ui.utils.Dumper.dumpNcmFile 39 | import java.io.File 40 | import java.text.SimpleDateFormat 41 | import java.util.Locale 42 | 43 | @SuppressLint("SimpleDateFormat", "ConstantLocale") 44 | val fmt = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault()) 45 | 46 | data class NcmFileInfo(val file: File) { 47 | val fileName: String = file.name 48 | val filePath: String = file.path 49 | val fileSize = file.length() 50 | val createDate: String = fmt.format(file.lastModified()) 51 | } 52 | 53 | @Composable 54 | private fun ConfirmDumpDialog(show: Boolean, fileInfo: NcmFileInfo, onDismiss: () -> Unit) { 55 | val sp = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) 56 | 57 | var exportResult by remember { mutableIntStateOf(-1) } 58 | var exportingDialogShow by remember { mutableStateOf(false) } 59 | var exportResultDialogShow by remember { mutableStateOf(false) } 60 | val execScope = rememberCoroutineScope() 61 | fun exec() { 62 | execScope.launch { 63 | exportingDialogShow = true 64 | withContext(Dispatchers.IO) { 65 | val beg = System.currentTimeMillis() 66 | val output = sp.getString("output_path", fileInfo.file.parent ?: "") ?: "" 67 | exportResult = dumpNcmFile(fileInfo.filePath, output) 68 | val end = System.currentTimeMillis() 69 | if (end - beg < 500) delay(500 - (end - beg)) 70 | } 71 | exportingDialogShow = false 72 | exportResultDialogShow = true 73 | } 74 | } 75 | 76 | if (show) 77 | AlertDialog(onDismissRequest = onDismiss, 78 | buttons = { 79 | Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) { 80 | TextButton(onClick = onDismiss) { 81 | Text(text = stringResource(id = R.string.cancel)) 82 | } 83 | TextButton(onClick = { 84 | exec() 85 | onDismiss() 86 | }) { 87 | Text(text = stringResource(id = R.string.confirm)) 88 | } 89 | } 90 | }, 91 | title = { 92 | Text(text = stringResource(id = R.string.title_dialog_confirm_dump)) 93 | }, 94 | text = { 95 | Text( 96 | text = stringResource(id = R.string.message_dialog_confirm_dump).format(fileInfo.fileName) 97 | ) 98 | }) 99 | 100 | if (exportingDialogShow) 101 | AlertDialog( 102 | onDismissRequest = { }, 103 | buttons = { 104 | Box( 105 | modifier = Modifier.padding(16.dp), 106 | contentAlignment = Alignment.Center 107 | ) { 108 | Column { 109 | CircularProgressIndicator() 110 | Text( 111 | modifier = Modifier.padding(top = 8.dp), 112 | text = stringResource(id = R.string.message_dialog_exporting) 113 | ) 114 | } 115 | } 116 | } 117 | ) 118 | 119 | if (exportResultDialogShow) 120 | AlertDialog(onDismissRequest = { exportResultDialogShow = false }, 121 | buttons = { 122 | Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) { 123 | TextButton(onClick = { exportResultDialogShow = false }) { 124 | Text(text = stringResource(id = R.string.confirm)) 125 | } 126 | } 127 | }, 128 | title = { 129 | Text(text = stringResource(id = if (exportResult == 0) R.string.title_dialog_dump_success else R.string.title_dialog_dump_failed)) 130 | }, 131 | text = { 132 | val resId = when (exportResult) { 133 | 0 -> R.string.message_dialog_dump_success 134 | 1 -> R.string.message_dialog_dump_failed_invalid_input_file 135 | 2 -> R.string.message_dialog_dump_failed_invalid_output_folder 136 | 3 -> R.string.message_dialog_dump_failed_file_not_ncm_file 137 | 4 -> R.string.message_dialog_dump_failed_file_unknow_format 138 | 5 -> R.string.message_dialog_dump_failed_file_cannot_read_music_info 139 | 6 -> R.string.message_dialog_dump_failed_file_cannot_read_music_cover 140 | 7 -> R.string.message_dialog_dump_failed_file_cannot_read_music_data 141 | 8 -> R.string.message_dialog_dump_failed_file_cannot_save_output_file 142 | else -> R.string.message_dialog_dump_failed_unknown 143 | } 144 | Text(text = stringResource(id = resId)) 145 | }) 146 | } 147 | 148 | @OptIn(ExperimentalFoundationApi::class) 149 | @Composable 150 | fun NcmFileItem(fileInfo: NcmFileInfo) { 151 | var dialogShow by remember { mutableStateOf(false) } 152 | ConfirmDumpDialog(show = dialogShow, fileInfo = fileInfo) { 153 | dialogShow = false 154 | } 155 | 156 | Card( 157 | modifier = Modifier 158 | .fillMaxWidth() 159 | .padding(start = 4.dp, end = 4.dp, top = 2.dp, bottom = 2.dp), 160 | ) { 161 | Box(modifier = Modifier.clickable { dialogShow = true }) { 162 | Column( 163 | modifier = Modifier 164 | .padding(8.dp), 165 | verticalArrangement = Arrangement.Center 166 | ) { 167 | Text( 168 | maxLines = 1, 169 | modifier = Modifier.basicMarquee(), 170 | text = fileInfo.fileName, 171 | fontSize = 18.sp 172 | ) 173 | Text( 174 | maxLines = 1, 175 | text = "${fileInfo.createDate} | ${String.format("%.2f", fileInfo.fileSize / 1024.00 / 1024.00)}MB", 176 | color = Color.Gray, 177 | fontSize = 14.sp 178 | ) 179 | } 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_arrow_back_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_more_vert_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_refresh_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_settings_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ncm Dumper 4 | 设置 5 | 确认 6 | 取消 7 | 授权访问所有文件 8 | 授予访问所有文件的权限来获取.ncm文件列表 9 | 刷新 10 | 导出歌曲 11 | 确认导出歌曲 %s ? 12 | 导出中… 13 | 导出成功 14 | 导出失败 15 | 已成功导出歌曲。 16 | 错误:无效的输入流! 17 | 错误:无效的输出文件夹! 18 | 错误:Ncm文件头校验失败,此文件可能不是.ncm格式的文件! 19 | 错误:未知的音乐格式! 20 | 错误:无法读取歌曲信息! 21 | 错误:无法读取歌曲封面信息! 22 | 错误:无法读取歌曲数据! 23 | 错误:无法保存输出文件! 24 | 错误:未知错误! 25 | 空空如也~ 26 | 搜索 27 | 返回 28 | 设置输出目录 29 | 全盘搜索 30 | 全盘搜索.ncm文件,否则仅搜索网易云音乐的下载目录。 31 | 导出 32 | 关于 33 | 版本 34 | 前往Github浏览项目 35 | 点个Star吧 :) 36 | 搜索… 37 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Ncm Dumper 3 | Settings 4 | Confirm 5 | Cancel 6 | Require all file access permission 7 | Grant all file access permisson to find .ncm files 8 | Refresh 9 | Dump music 10 | Do you want to dump music %s ? 11 | Dumping… 12 | Dump succeed 13 | Dump failed 14 | Dump music file succeed. 15 | Error: Invalid input stream! 16 | Error: Invalid output folder 17 | Error: Ncm header check failed, this file may not .ncm file! 18 | Error: Unknown music file format! 19 | Error: Cannot read music info! 20 | Error: Cannot read music cover data! 21 | Error: Cannot read music data! 22 | Error: Cannot save output file! 23 | Error: Unknown error! 24 | Nothing here~ 25 | Search 26 | Search… 27 | Back 28 | Set output folder 29 | Search full disk 30 | Search .ncm files in the storage, otherwise only search netease cloud music download dir. 31 | Dumper 32 | About 33 | Version 34 | View project on github 35 | Star me plz :) 36 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |