diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 000000000..a6df3a164 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,69 @@ +name: Preview Build +on: + workflow_dispatch: + inputs: +jobs: + libcore: + name: Native Build (LibCore) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Golang Status + run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status + - name: Libcore Status + run: git ls-files libcore | xargs cat | sha1sum > libcore_status + - name: LibCore Cache + id: cache + uses: actions/cache@v4 + with: + path: | + app/libs/libcore.aar + key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }} + - name: Install Golang + if: steps.cache.outputs.cache-hit != 'true' + uses: actions/setup-go@v5 + with: + go-version: ^1.25 + - name: Native Build + if: steps.cache.outputs.cache-hit != 'true' + run: ./run lib core + build: + name: Build OSS APK + runs-on: ubuntu-latest + needs: + - libcore + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Golang Status + run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status + - name: Libcore Status + run: git ls-files libcore | xargs cat | sha1sum > libcore_status + - name: LibCore Cache + uses: actions/cache@v4 + with: + path: | + app/libs/libcore.aar + key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }} + - name: Gradle cache + uses: actions/cache@v4 + with: + path: ~/.gradle + key: gradle-oss-${{ hashFiles('**/*.gradle.kts') }} + - name: Gradle Build + env: + BUILD_PLUGIN: none + run: | + echo "sdk.dir=${ANDROID_HOME}" > local.properties + echo "ndk.dir=${ANDROID_HOME}/ndk/25.0.8775105" >> local.properties + export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}" + ./run init action gradle + ./gradlew app:assemblePreviewRelease + APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk') + APK=$(dirname $APK) + echo "APK=$APK" >> $GITHUB_ENV + - uses: actions/upload-artifact@v4 + with: + name: APKs + path: ${{ env.APK }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0cd234122..7aecc1c05 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' uses: actions/setup-go@v5 with: - go-version: ^1.24 + go-version: ^1.25 - name: Native Build if: steps.cache.outputs.cache-hit != 'true' run: ./run lib core diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d8d3dac03..a8ba7b0e2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -45,7 +45,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.recyclerview:recyclerview:1.3.0") - implementation("androidx.activity:activity-ktx:1.7.0") + implementation("androidx.activity:activity-ktx:1.10.1") implementation("androidx.fragment:fragment-ktx:1.5.6") implementation("androidx.browser:browser:1.5.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") diff --git a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/6.json b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/6.json new file mode 100644 index 000000000..11b6e58a2 --- /dev/null +++ b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/6.json @@ -0,0 +1,373 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "3d3db9106a89d6f20ef3fde6e81dbaa9", + "entities": [ + { + "tableName": "proxy_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL, `isSelector` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL, `landingProxy` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ungrouped", + "columnName": "ungrouped", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscription", + "columnName": "subscription", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSelector", + "columnName": "isSelector", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "frontProxy", + "columnName": "frontProxy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "landingProxy", + "columnName": "landingProxy", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "proxy_entities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `vmessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `mieruBean` BLOB, `naiveBean` BLOB, `hysteriaBean` BLOB, `tuicBean` BLOB, `sshBean` BLOB, `wgBean` BLOB, `shadowTLSBean` BLOB, `anyTLSBean` BLOB, `chainBean` BLOB, `nekoBean` BLOB, `configBean` BLOB)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tx", + "columnName": "tx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rx", + "columnName": "rx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ping", + "columnName": "ping", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "socksBean", + "columnName": "socksBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "httpBean", + "columnName": "httpBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "ssBean", + "columnName": "ssBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "vmessBean", + "columnName": "vmessBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanBean", + "columnName": "trojanBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "trojanGoBean", + "columnName": "trojanGoBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "mieruBean", + "columnName": "mieruBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "naiveBean", + "columnName": "naiveBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "hysteriaBean", + "columnName": "hysteriaBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "tuicBean", + "columnName": "tuicBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "sshBean", + "columnName": "sshBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "wgBean", + "columnName": "wgBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "shadowTLSBean", + "columnName": "shadowTLSBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "anyTLSBean", + "columnName": "anyTLSBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "chainBean", + "columnName": "chainBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "nekoBean", + "columnName": "nekoBean", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "configBean", + "columnName": "configBean", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `config` TEXT NOT NULL DEFAULT '', `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `packages` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domains", + "columnName": "domains", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ip", + "columnName": "ip", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourcePort", + "columnName": "sourcePort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "network", + "columnName": "network", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "outbound", + "columnName": "outbound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packages", + "columnName": "packages", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3d3db9106a89d6f20ef3fde6e81dbaa9')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f969dea48..69818c852 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,7 +22,9 @@ + tools:ignore="PackageVisibilityPolicy" /> + + @@ -219,6 +221,24 @@ + + - * This implementation was imported to WireGuard from noise-java: - * https://github.com/rweather/noise-java - *

- * This implementation is based on that from arduinolibs: - * https://github.com/rweather/arduinolibs - *

- * Differences in this version are due to using 26-bit limbs for the - * representation instead of the 8/16/32-bit limbs in the original. - *

- * References: http://cr.yp.to/ecdh.html, RFC 7748 - */ -@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"}) -public final class Curve25519 { - // Numbers modulo 2^255 - 19 are broken up into ten 26-bit words. - private static final int NUM_LIMBS_255BIT = 10; - private static final int NUM_LIMBS_510BIT = 20; - - private final int[] A; - private final int[] AA; - private final int[] B; - private final int[] BB; - private final int[] C; - private final int[] CB; - private final int[] D; - private final int[] DA; - private final int[] E; - private final long[] t1; - private final int[] t2; - private final int[] x_1; - private final int[] x_2; - private final int[] x_3; - private final int[] z_2; - private final int[] z_3; - - /** - * Constructs the temporary state holder for Curve25519 evaluation. - */ - private Curve25519() { - // Allocate memory for all of the temporary variables we will need. - x_1 = new int[NUM_LIMBS_255BIT]; - x_2 = new int[NUM_LIMBS_255BIT]; - x_3 = new int[NUM_LIMBS_255BIT]; - z_2 = new int[NUM_LIMBS_255BIT]; - z_3 = new int[NUM_LIMBS_255BIT]; - A = new int[NUM_LIMBS_255BIT]; - B = new int[NUM_LIMBS_255BIT]; - C = new int[NUM_LIMBS_255BIT]; - D = new int[NUM_LIMBS_255BIT]; - E = new int[NUM_LIMBS_255BIT]; - AA = new int[NUM_LIMBS_255BIT]; - BB = new int[NUM_LIMBS_255BIT]; - DA = new int[NUM_LIMBS_255BIT]; - CB = new int[NUM_LIMBS_255BIT]; - t1 = new long[NUM_LIMBS_510BIT]; - t2 = new int[NUM_LIMBS_510BIT]; - } - - /** - * Conditional swap of two values. - * - * @param select Set to 1 to swap, 0 to leave as-is. - * @param x The first value. - * @param y The second value. - */ - private static void cswap(int select, final int[] x, final int[] y) { - select = -select; - for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { - final int dummy = select & (x[index] ^ y[index]); - x[index] ^= dummy; - y[index] ^= dummy; - } - } - - /** - * Evaluates the Curve25519 curve. - * - * @param result Buffer to place the result of the evaluation into. - * @param offset Offset into the result buffer. - * @param privateKey The private key to use in the evaluation. - * @param publicKey The public key to use in the evaluation, or null - * if the base point of the curve should be used. - */ - public static void eval(final byte[] result, final int offset, - final byte[] privateKey, @Nullable final byte[] publicKey) { - final Curve25519 state = new Curve25519(); - try { - // Unpack the public key value. If null, use 9 as the base point. - Arrays.fill(state.x_1, 0); - if (publicKey != null) { - // Convert the input value from little-endian into 26-bit limbs. - for (int index = 0; index < 32; ++index) { - final int bit = (index * 8) % 26; - final int word = (index * 8) / 26; - final int value = publicKey[index] & 0xFF; - if (bit <= (26 - 8)) { - state.x_1[word] |= value << bit; - } else { - state.x_1[word] |= value << bit; - state.x_1[word] &= 0x03FFFFFF; - state.x_1[word + 1] |= value >> (26 - bit); - } - } - - // Just in case, we reduce the number modulo 2^255 - 19 to - // make sure that it is in range of the field before we start. - // This eliminates values between 2^255 - 19 and 2^256 - 1. - state.reduceQuick(state.x_1); - state.reduceQuick(state.x_1); - } else { - state.x_1[0] = 9; - } - - // Initialize the other temporary variables. - Arrays.fill(state.x_2, 0); // x_2 = 1 - state.x_2[0] = 1; - Arrays.fill(state.z_2, 0); // z_2 = 0 - System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length); // x_3 = x_1 - Arrays.fill(state.z_3, 0); // z_3 = 1 - state.z_3[0] = 1; - - // Evaluate the curve for every bit of the private key. - state.evalCurve(privateKey); - - // Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19. - state.recip(state.z_3, state.z_2); - state.mul(state.x_2, state.x_2, state.z_3); - - // Convert x_2 into little-endian in the result buffer. - for (int index = 0; index < 32; ++index) { - final int bit = (index * 8) % 26; - final int word = (index * 8) / 26; - if (bit <= (26 - 8)) - result[offset + index] = (byte) (state.x_2[word] >> bit); - else - result[offset + index] = (byte) ((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit))); - } - } finally { - // Clean up all temporary state before we exit. - state.destroy(); - } - } - - /** - * Subtracts two numbers modulo 2^255 - 19. - * - * @param result The result. - * @param x The first number to subtract. - * @param y The second number to subtract. - */ - private static void sub(final int[] result, final int[] x, final int[] y) { - int index; - int borrow; - - // Subtract y from x to generate the intermediate result. - borrow = 0; - for (index = 0; index < NUM_LIMBS_255BIT; ++index) { - borrow = x[index] - y[index] - ((borrow >> 26) & 0x01); - result[index] = borrow & 0x03FFFFFF; - } - - // If we had a borrow, then the result has gone negative and we - // have to add 2^255 - 19 to the result to make it positive again. - // The top bits of "borrow" will be all 1's if there is a borrow - // or it will be all 0's if there was no borrow. Easiest is to - // conditionally subtract 19 and then mask off the high bits. - borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19); - result[0] = borrow & 0x03FFFFFF; - for (index = 1; index < NUM_LIMBS_255BIT; ++index) { - borrow = result[index] - ((borrow >> 26) & 0x01); - result[index] = borrow & 0x03FFFFFF; - } - result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; - } - - /** - * Adds two numbers modulo 2^255 - 19. - * - * @param result The result. - * @param x The first number to add. - * @param y The second number to add. - */ - private void add(final int[] result, final int[] x, final int[] y) { - int carry = x[0] + y[0]; - result[0] = carry & 0x03FFFFFF; - for (int index = 1; index < NUM_LIMBS_255BIT; ++index) { - carry = (carry >> 26) + x[index] + y[index]; - result[index] = carry & 0x03FFFFFF; - } - reduceQuick(result); - } - - /** - * Destroy all sensitive data in this object. - */ - private void destroy() { - // Destroy all temporary variables. - Arrays.fill(x_1, 0); - Arrays.fill(x_2, 0); - Arrays.fill(x_3, 0); - Arrays.fill(z_2, 0); - Arrays.fill(z_3, 0); - Arrays.fill(A, 0); - Arrays.fill(B, 0); - Arrays.fill(C, 0); - Arrays.fill(D, 0); - Arrays.fill(E, 0); - Arrays.fill(AA, 0); - Arrays.fill(BB, 0); - Arrays.fill(DA, 0); - Arrays.fill(CB, 0); - Arrays.fill(t1, 0L); - Arrays.fill(t2, 0); - } - - /** - * Evaluates the curve for every bit in a secret key. - * - * @param s The 32-byte secret key. - */ - private void evalCurve(final byte[] s) { - int sposn = 31; - int sbit = 6; - int svalue = s[sposn] | 0x40; - int swap = 0; - - // Iterate over all 255 bits of "s" from the highest to the lowest. - // We ignore the high bit of the 256-bit representation of "s". - while (true) { - // Conditional swaps on entry to this bit but only if we - // didn't swap on the previous bit. - final int select = (svalue >> sbit) & 0x01; - swap ^= select; - cswap(swap, x_2, x_3); - cswap(swap, z_2, z_3); - swap = select; - - // Evaluate the curve. - add(A, x_2, z_2); // A = x_2 + z_2 - square(AA, A); // AA = A^2 - sub(B, x_2, z_2); // B = x_2 - z_2 - square(BB, B); // BB = B^2 - sub(E, AA, BB); // E = AA - BB - add(C, x_3, z_3); // C = x_3 + z_3 - sub(D, x_3, z_3); // D = x_3 - z_3 - mul(DA, D, A); // DA = D * A - mul(CB, C, B); // CB = C * B - add(x_3, DA, CB); // x_3 = (DA + CB)^2 - square(x_3, x_3); - sub(z_3, DA, CB); // z_3 = x_1 * (DA - CB)^2 - square(z_3, z_3); - mul(z_3, z_3, x_1); - mul(x_2, AA, BB); // x_2 = AA * BB - mulA24(z_2, E); // z_2 = E * (AA + a24 * E) - add(z_2, z_2, AA); - mul(z_2, z_2, E); - - // Move onto the next lower bit of "s". - if (sbit > 0) { - --sbit; - } else if (sposn == 0) { - break; - } else if (sposn == 1) { - --sposn; - svalue = s[sposn] & 0xF8; - sbit = 7; - } else { - --sposn; - svalue = s[sposn]; - sbit = 7; - } - } - - // Final conditional swaps. - cswap(swap, x_2, x_3); - cswap(swap, z_2, z_3); - } - - /** - * Multiplies two numbers modulo 2^255 - 19. - * - * @param result The result. - * @param x The first number to multiply. - * @param y The second number to multiply. - */ - private void mul(final int[] result, final int[] x, final int[] y) { - // Multiply the two numbers to create the intermediate result. - long v = x[0]; - for (int i = 0; i < NUM_LIMBS_255BIT; ++i) { - t1[i] = v * y[i]; - } - for (int i = 1; i < NUM_LIMBS_255BIT; ++i) { - v = x[i]; - for (int j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) { - t1[i + j] += v * y[j]; - } - t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1]; - } - - // Propagate carries and convert back into 26-bit words. - v = t1[0]; - t2[0] = ((int) v) & 0x03FFFFFF; - for (int i = 1; i < NUM_LIMBS_510BIT; ++i) { - v = (v >> 26) + t1[i]; - t2[i] = ((int) v) & 0x03FFFFFF; - } - - // Reduce the result modulo 2^255 - 19. - reduce(result, t2, NUM_LIMBS_255BIT); - } - - /** - * Multiplies a number by the a24 constant, modulo 2^255 - 19. - * - * @param result The result. - * @param x The number to multiply by a24. - */ - private void mulA24(final int[] result, final int[] x) { - final long a24 = 121665; - long carry = 0; - for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { - carry += a24 * x[index]; - t2[index] = ((int) carry) & 0x03FFFFFF; - carry >>= 26; - } - t2[NUM_LIMBS_255BIT] = ((int) carry) & 0x03FFFFFF; - reduce(result, t2, 1); - } - - /** - * Raise x to the power of (2^250 - 1). - * - * @param result The result. Must not overlap with x. - * @param x The argument. - */ - private void pow250(final int[] result, final int[] x) { - // The big-endian hexadecimal expansion of (2^250 - 1) is: - // 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF - // - // The naive implementation needs to do 2 multiplications per 1 bit and - // 1 multiplication per 0 bit. We can improve upon this by creating a - // pattern 0000000001 ... 0000000001. If we square and multiply the - // pattern by itself we can turn the pattern into the partial results - // 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc. - // This averages out to about 1.1 multiplications per 1 bit instead of 2. - - // Build a pattern of 250 bits in length of repeated copies of 0000000001. - square(A, x); - for (int j = 0; j < 9; ++j) - square(A, A); - mul(result, A, x); - for (int i = 0; i < 23; ++i) { - for (int j = 0; j < 10; ++j) - square(A, A); - mul(result, result, A); - } - - // Multiply bit-shifted versions of the 0000000001 pattern into - // the result to "fill in" the gaps in the pattern. - square(A, result); - mul(result, result, A); - for (int j = 0; j < 8; ++j) { - square(A, A); - mul(result, result, A); - } - } - - /** - * Computes the reciprocal of a number modulo 2^255 - 19. - * - * @param result The result. Must not overlap with x. - * @param x The argument. - */ - private void recip(final int[] result, final int[] x) { - // The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19. - // The big-endian hexadecimal expansion of (p - 2) is: - // 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB - // Start with the 250 upper bits of the expansion of (p - 2). - pow250(result, x); - - // Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest. - square(result, result); - square(result, result); - mul(result, result, x); - square(result, result); - square(result, result); - mul(result, result, x); - square(result, result); - mul(result, result, x); - } - - /** - * Reduce a number modulo 2^255 - 19. - * - * @param result The result. - * @param x The value to be reduced. This array will be - * modified during the reduction. - * @param size The number of limbs in the high order half of x. - */ - private void reduce(final int[] result, final int[] x, final int size) { - // Calculate (x mod 2^255) + ((x / 2^255) * 19) which will - // either produce the answer we want or it will produce a - // value of the form "answer + j * (2^255 - 19)". There are - // 5 left-over bits in the top-most limb of the bottom half. - int carry = 0; - int limb = x[NUM_LIMBS_255BIT - 1] >> 21; - x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; - for (int index = 0; index < size; ++index) { - limb += x[NUM_LIMBS_255BIT + index] << 5; - carry += (limb & 0x03FFFFFF) * 19 + x[index]; - x[index] = carry & 0x03FFFFFF; - limb >>= 26; - carry >>= 26; - } - if (size < NUM_LIMBS_255BIT) { - // The high order half of the number is short; e.g. for mulA24(). - // Propagate the carry through the rest of the low order part. - for (int index = size; index < NUM_LIMBS_255BIT; ++index) { - carry += x[index]; - x[index] = carry & 0x03FFFFFF; - carry >>= 26; - } - } - - // The "j" value may still be too large due to the final carry-out. - // We must repeat the reduction. If we already have the answer, - // then this won't do any harm but we must still do the calculation - // to preserve the overall timing. The "j" value will be between - // 0 and 19, which means that the carry we care about is in the - // top 5 bits of the highest limb of the bottom half. - carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19; - x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; - for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { - carry += x[index]; - result[index] = carry & 0x03FFFFFF; - carry >>= 26; - } - - // At this point "x" will either be the answer or it will be the - // answer plus (2^255 - 19). Perform a trial subtraction to - // complete the reduction process. - reduceQuick(result); - } - - /** - * Reduces a number modulo 2^255 - 19 where it is known that the - * number can be reduced with only 1 trial subtraction. - * - * @param x The number to reduce, and the result. - */ - private void reduceQuick(final int[] x) { - // Perform a trial subtraction of (2^255 - 19) from "x" which is - // equivalent to adding 19 and subtracting 2^255. We add 19 here; - // the subtraction of 2^255 occurs in the next step. - int carry = 19; - for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { - carry += x[index]; - t2[index] = carry & 0x03FFFFFF; - carry >>= 26; - } - - // If there was a borrow, then the original "x" is the correct answer. - // If there was no borrow, then "t2" is the correct answer. Select the - // correct answer but do it in a way that instruction timing will not - // reveal which value was selected. Borrow will occur if bit 21 of - // "t2" is zero. Turn the bit into a selection mask. - final int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01); - final int nmask = ~mask; - t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; - for (int index = 0; index < NUM_LIMBS_255BIT; ++index) - x[index] = (x[index] & nmask) | (t2[index] & mask); - } - - /** - * Squares a number modulo 2^255 - 19. - * - * @param result The result. - * @param x The number to square. - */ - private void square(final int[] result, final int[] x) { - mul(result, x, x); - } -} diff --git a/app/src/main/java/com/wireguard/crypto/Ed25519.java b/app/src/main/java/com/wireguard/crypto/Ed25519.java deleted file mode 100644 index a60babfbb..000000000 --- a/app/src/main/java/com/wireguard/crypto/Ed25519.java +++ /dev/null @@ -1,2508 +0,0 @@ -/* - * Copyright © 2020 WireGuard LLC. All Rights Reserved. - * Copyright 2017 Google Inc. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.crypto; - -import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.util.Arrays; - -/** - * Implementation of Ed25519 signature verification. - * - *

This implementation is based on the ed25519/ref10 implementation in NaCl.

- * - *

It implements this twisted Edwards curve: - * - *

- * -x^2 + y^2 = 1 + (-121665 / 121666 mod 2^255-19)*x^2*y^2
- * 
- * - * @see Bernstein D.J., Birkner P., Joye M., Lange - * T., Peters C. (2008) Twisted Edwards Curves - * @see Hisil H., Wong K.KH., Carter G., Dawson E. - * (2008) Twisted Edwards Curves Revisited - */ -public final class Ed25519 { - - // d = -121665 / 121666 mod 2^255-19 - private static final long[] D; - // 2d - private static final long[] D2; - // 2^((p-1)/4) mod p where p = 2^255-19 - private static final long[] SQRTM1; - - /** - * Base point for the Edwards twisted curve = (x, 4/5) and its exponentiations. B_TABLE[i][j] = - * (j+1)*256^i*B for i in [0, 32) and j in [0, 8). Base point B = B_TABLE[0][0] - */ - private static final CachedXYT[][] B_TABLE; - private static final CachedXYT[] B2; - - private static final BigInteger P_BI = - BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19)); - private static final BigInteger D_BI = - BigInteger.valueOf(-121665).multiply(BigInteger.valueOf(121666).modInverse(P_BI)).mod(P_BI); - private static final BigInteger D2_BI = BigInteger.valueOf(2).multiply(D_BI).mod(P_BI); - private static final BigInteger SQRTM1_BI = - BigInteger.valueOf(2).modPow(P_BI.subtract(BigInteger.ONE).divide(BigInteger.valueOf(4)), P_BI); - - private Ed25519() { - } - - private static class Point { - private BigInteger x; - private BigInteger y; - } - - private static BigInteger recoverX(BigInteger y) { - // x^2 = (y^2 - 1) / (d * y^2 + 1) mod 2^255-19 - BigInteger xx = - y.pow(2) - .subtract(BigInteger.ONE) - .multiply(D_BI.multiply(y.pow(2)).add(BigInteger.ONE).modInverse(P_BI)); - BigInteger x = xx.modPow(P_BI.add(BigInteger.valueOf(3)).divide(BigInteger.valueOf(8)), P_BI); - if (!x.pow(2).subtract(xx).mod(P_BI).equals(BigInteger.ZERO)) { - x = x.multiply(SQRTM1_BI).mod(P_BI); - } - if (x.testBit(0)) { - x = P_BI.subtract(x); - } - return x; - } - - private static Point edwards(Point a, Point b) { - Point o = new Point(); - BigInteger xxyy = D_BI.multiply(a.x.multiply(b.x).multiply(a.y).multiply(b.y)).mod(P_BI); - o.x = - (a.x.multiply(b.y).add(b.x.multiply(a.y))) - .multiply(BigInteger.ONE.add(xxyy).modInverse(P_BI)) - .mod(P_BI); - o.y = - (a.y.multiply(b.y).add(a.x.multiply(b.x))) - .multiply(BigInteger.ONE.subtract(xxyy).modInverse(P_BI)) - .mod(P_BI); - return o; - } - - private static byte[] toLittleEndian(BigInteger n) { - byte[] b = new byte[32]; - byte[] nBytes = n.toByteArray(); - System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length); - for (int i = 0; i < b.length / 2; i++) { - byte t = b[i]; - b[i] = b[b.length - i - 1]; - b[b.length - i - 1] = t; - } - return b; - } - - private static CachedXYT getCachedXYT(Point p) { - return new CachedXYT( - Field25519.expand(toLittleEndian(p.y.add(p.x).mod(P_BI))), - Field25519.expand(toLittleEndian(p.y.subtract(p.x).mod(P_BI))), - Field25519.expand(toLittleEndian(D2_BI.multiply(p.x).multiply(p.y).mod(P_BI)))); - } - - static { - Point b = new Point(); - b.y = BigInteger.valueOf(4).multiply(BigInteger.valueOf(5).modInverse(P_BI)).mod(P_BI); - b.x = recoverX(b.y); - - D = Field25519.expand(toLittleEndian(D_BI)); - D2 = Field25519.expand(toLittleEndian(D2_BI)); - SQRTM1 = Field25519.expand(toLittleEndian(SQRTM1_BI)); - - Point bi = b; - B_TABLE = new CachedXYT[32][8]; - for (int i = 0; i < 32; i++) { - Point bij = bi; - for (int j = 0; j < 8; j++) { - B_TABLE[i][j] = getCachedXYT(bij); - bij = edwards(bij, bi); - } - for (int j = 0; j < 8; j++) { - bi = edwards(bi, bi); - } - } - bi = b; - Point b2 = edwards(b, b); - B2 = new CachedXYT[8]; - for (int i = 0; i < 8; i++) { - B2[i] = getCachedXYT(bi); - bi = edwards(bi, b2); - } - } - - private static final int PUBLIC_KEY_LEN = Field25519.FIELD_LEN; - private static final int SIGNATURE_LEN = Field25519.FIELD_LEN * 2; - - /** - * Defines field 25519 function based on curve25519-donna C - * implementation (mostly identical). - * - *

Field elements are written as an array of signed, 64-bit limbs (an array of longs), least - * significant first. The value of the field element is: - * - *

-     * x[0] + 2^26·x[1] + 2^51·x[2] + 2^77·x[3] + 2^102·x[4] + 2^128·x[5] + 2^153·x[6] + 2^179·x[7] +
-     * 2^204·x[8] + 2^230·x[9],
-     * 
- * - *

i.e. the limbs are 26, 25, 26, 25, ... bits wide. - */ - private static final class Field25519 { - /** - * During Field25519 computation, the mixed radix representation may be in different forms: - *

    - *
  • Reduced-size form: the array has size at most 10. - *
  • Non-reduced-size form: the array is not reduced modulo 2^255 - 19 and has size at most - * 19. - *
- *

- * TODO(quannguyen): - *

    - *
  • Clarify ill-defined terminologies. - *
  • The reduction procedure is different from DJB's paper - * (http://cr.yp.to/ecdh/curve25519-20060209.pdf). The coefficients after reducing degree and - * reducing coefficients aren't guaranteed to be in range {-2^25, ..., 2^25}. We should check to - * see what's going on. - *
  • Consider using method mult() everywhere and making product() private. - *
- */ - - static final int FIELD_LEN = 32; - static final int LIMB_CNT = 10; - private static final long TWO_TO_25 = 1 << 25; - private static final long TWO_TO_26 = TWO_TO_25 << 1; - - private static final int[] EXPAND_START = {0, 3, 6, 9, 12, 16, 19, 22, 25, 28}; - private static final int[] EXPAND_SHIFT = {0, 2, 3, 5, 6, 0, 1, 3, 4, 6}; - private static final int[] MASK = {0x3ffffff, 0x1ffffff}; - private static final int[] SHIFT = {26, 25}; - - /** - * Sums two numbers: output = in1 + in2 - *

- * On entry: in1, in2 are in reduced-size form. - */ - static void sum(long[] output, long[] in1, long[] in2) { - for (int i = 0; i < LIMB_CNT; i++) { - output[i] = in1[i] + in2[i]; - } - } - - /** - * Sums two numbers: output += in - *

- * On entry: in is in reduced-size form. - */ - static void sum(long[] output, long[] in) { - sum(output, output, in); - } - - /** - * Find the difference of two numbers: output = in1 - in2 - * (note the order of the arguments!). - *

- * On entry: in1, in2 are in reduced-size form. - */ - static void sub(long[] output, long[] in1, long[] in2) { - for (int i = 0; i < LIMB_CNT; i++) { - output[i] = in1[i] - in2[i]; - } - } - - /** - * Find the difference of two numbers: output = in - output - * (note the order of the arguments!). - *

- * On entry: in, output are in reduced-size form. - */ - static void sub(long[] output, long[] in) { - sub(output, in, output); - } - - /** - * Multiply a number by a scalar: output = in * scalar - */ - static void scalarProduct(long[] output, long[] in, long scalar) { - for (int i = 0; i < LIMB_CNT; i++) { - output[i] = in[i] * scalar; - } - } - - /** - * Multiply two numbers: out = in2 * in - *

- * output must be distinct to both inputs. The inputs are reduced coefficient form, - * the output is not. - *

- * out[x] <= 14 * the largest product of the input limbs. - */ - static void product(long[] out, long[] in2, long[] in) { - out[0] = in2[0] * in[0]; - out[1] = in2[0] * in[1] - + in2[1] * in[0]; - out[2] = 2 * in2[1] * in[1] - + in2[0] * in[2] - + in2[2] * in[0]; - out[3] = in2[1] * in[2] - + in2[2] * in[1] - + in2[0] * in[3] - + in2[3] * in[0]; - out[4] = in2[2] * in[2] - + 2 * (in2[1] * in[3] + in2[3] * in[1]) - + in2[0] * in[4] - + in2[4] * in[0]; - out[5] = in2[2] * in[3] - + in2[3] * in[2] - + in2[1] * in[4] - + in2[4] * in[1] - + in2[0] * in[5] - + in2[5] * in[0]; - out[6] = 2 * (in2[3] * in[3] + in2[1] * in[5] + in2[5] * in[1]) - + in2[2] * in[4] - + in2[4] * in[2] - + in2[0] * in[6] - + in2[6] * in[0]; - out[7] = in2[3] * in[4] - + in2[4] * in[3] - + in2[2] * in[5] - + in2[5] * in[2] - + in2[1] * in[6] - + in2[6] * in[1] - + in2[0] * in[7] - + in2[7] * in[0]; - out[8] = in2[4] * in[4] - + 2 * (in2[3] * in[5] + in2[5] * in[3] + in2[1] * in[7] + in2[7] * in[1]) - + in2[2] * in[6] - + in2[6] * in[2] - + in2[0] * in[8] - + in2[8] * in[0]; - out[9] = in2[4] * in[5] - + in2[5] * in[4] - + in2[3] * in[6] - + in2[6] * in[3] - + in2[2] * in[7] - + in2[7] * in[2] - + in2[1] * in[8] - + in2[8] * in[1] - + in2[0] * in[9] - + in2[9] * in[0]; - out[10] = - 2 * (in2[5] * in[5] + in2[3] * in[7] + in2[7] * in[3] + in2[1] * in[9] + in2[9] * in[1]) - + in2[4] * in[6] - + in2[6] * in[4] - + in2[2] * in[8] - + in2[8] * in[2]; - out[11] = in2[5] * in[6] - + in2[6] * in[5] - + in2[4] * in[7] - + in2[7] * in[4] - + in2[3] * in[8] - + in2[8] * in[3] - + in2[2] * in[9] - + in2[9] * in[2]; - out[12] = in2[6] * in[6] - + 2 * (in2[5] * in[7] + in2[7] * in[5] + in2[3] * in[9] + in2[9] * in[3]) - + in2[4] * in[8] - + in2[8] * in[4]; - out[13] = in2[6] * in[7] - + in2[7] * in[6] - + in2[5] * in[8] - + in2[8] * in[5] - + in2[4] * in[9] - + in2[9] * in[4]; - out[14] = 2 * (in2[7] * in[7] + in2[5] * in[9] + in2[9] * in[5]) - + in2[6] * in[8] - + in2[8] * in[6]; - out[15] = in2[7] * in[8] - + in2[8] * in[7] - + in2[6] * in[9] - + in2[9] * in[6]; - out[16] = in2[8] * in[8] - + 2 * (in2[7] * in[9] + in2[9] * in[7]); - out[17] = in2[8] * in[9] - + in2[9] * in[8]; - out[18] = 2 * in2[9] * in[9]; - } - - /** - * Reduce a field element by calling reduceSizeByModularReduction and reduceCoefficients. - * - * @param input An input array of any length. If the array has 19 elements, it will be used as - * temporary buffer and its contents changed. - * @param output An output array of size LIMB_CNT. After the call |output[i]| < 2^26 will hold. - */ - static void reduce(long[] input, long[] output) { - long[] tmp; - if (input.length == 19) { - tmp = input; - } else { - tmp = new long[19]; - System.arraycopy(input, 0, tmp, 0, input.length); - } - reduceSizeByModularReduction(tmp); - reduceCoefficients(tmp); - System.arraycopy(tmp, 0, output, 0, LIMB_CNT); - } - - /** - * Reduce a long form to a reduced-size form by taking the input mod 2^255 - 19. - *

- * On entry: |output[i]| < 14*2^54 - * On exit: |output[0..8]| < 280*2^54 - */ - static void reduceSizeByModularReduction(long[] output) { - // The coefficients x[10], x[11],..., x[18] are eliminated by reduction modulo 2^255 - 19. - // For example, the coefficient x[18] is multiplied by 19 and added to the coefficient x[8]. - // - // Each of these shifts and adds ends up multiplying the value by 19. - // - // For output[0..8], the absolute entry value is < 14*2^54 and we add, at most, 19*14*2^54 thus, - // on exit, |output[0..8]| < 280*2^54. - output[8] += output[18] << 4; - output[8] += output[18] << 1; - output[8] += output[18]; - output[7] += output[17] << 4; - output[7] += output[17] << 1; - output[7] += output[17]; - output[6] += output[16] << 4; - output[6] += output[16] << 1; - output[6] += output[16]; - output[5] += output[15] << 4; - output[5] += output[15] << 1; - output[5] += output[15]; - output[4] += output[14] << 4; - output[4] += output[14] << 1; - output[4] += output[14]; - output[3] += output[13] << 4; - output[3] += output[13] << 1; - output[3] += output[13]; - output[2] += output[12] << 4; - output[2] += output[12] << 1; - output[2] += output[12]; - output[1] += output[11] << 4; - output[1] += output[11] << 1; - output[1] += output[11]; - output[0] += output[10] << 4; - output[0] += output[10] << 1; - output[0] += output[10]; - } - - /** - * Reduce all coefficients of the short form input so that |x| < 2^26. - *

- * On entry: |output[i]| < 280*2^54 - */ - static void reduceCoefficients(long[] output) { - output[10] = 0; - - for (int i = 0; i < LIMB_CNT; i += 2) { - long over = output[i] / TWO_TO_26; - // The entry condition (that |output[i]| < 280*2^54) means that over is, at most, 280*2^28 in - // the first iteration of this loop. This is added to the next limb and we can approximate the - // resulting bound of that limb by 281*2^54. - output[i] -= over << 26; - output[i + 1] += over; - - // For the first iteration, |output[i+1]| < 281*2^54, thus |over| < 281*2^29. When this is - // added to the next limb, the resulting bound can be approximated as 281*2^54. - // - // For subsequent iterations of the loop, 281*2^54 remains a conservative bound and no - // overflow occurs. - over = output[i + 1] / TWO_TO_25; - output[i + 1] -= over << 25; - output[i + 2] += over; - } - // Now |output[10]| < 281*2^29 and all other coefficients are reduced. - output[0] += output[10] << 4; - output[0] += output[10] << 1; - output[0] += output[10]; - - output[10] = 0; - // Now output[1..9] are reduced, and |output[0]| < 2^26 + 19*281*2^29 so |over| will be no more - // than 2^16. - long over = output[0] / TWO_TO_26; - output[0] -= over << 26; - output[1] += over; - // Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 2^16 < 2^26. The bound on - // |output[1]| is sufficient to meet our needs. - } - - /** - * A helpful wrapper around {@ref Field25519#product}: output = in * in2. - *

- * On entry: |in[i]| < 2^27 and |in2[i]| < 2^27. - *

- * The output is reduced degree (indeed, one need only provide storage for 10 limbs) and - * |output[i]| < 2^26. - */ - static void mult(long[] output, long[] in, long[] in2) { - long[] t = new long[19]; - product(t, in, in2); - // |t[i]| < 2^26 - reduce(t, output); - } - - /** - * Square a number: out = in**2 - *

- * output must be distinct from the input. The inputs are reduced coefficient form, the output is - * not. - *

- * out[x] <= 14 * the largest product of the input limbs. - */ - private static void squareInner(long[] out, long[] in) { - out[0] = in[0] * in[0]; - out[1] = 2 * in[0] * in[1]; - out[2] = 2 * (in[1] * in[1] + in[0] * in[2]); - out[3] = 2 * (in[1] * in[2] + in[0] * in[3]); - out[4] = in[2] * in[2] - + 4 * in[1] * in[3] - + 2 * in[0] * in[4]; - out[5] = 2 * (in[2] * in[3] + in[1] * in[4] + in[0] * in[5]); - out[6] = 2 * (in[3] * in[3] + in[2] * in[4] + in[0] * in[6] + 2 * in[1] * in[5]); - out[7] = 2 * (in[3] * in[4] + in[2] * in[5] + in[1] * in[6] + in[0] * in[7]); - out[8] = in[4] * in[4] - + 2 * (in[2] * in[6] + in[0] * in[8] + 2 * (in[1] * in[7] + in[3] * in[5])); - out[9] = 2 * (in[4] * in[5] + in[3] * in[6] + in[2] * in[7] + in[1] * in[8] + in[0] * in[9]); - out[10] = 2 * (in[5] * in[5] - + in[4] * in[6] - + in[2] * in[8] - + 2 * (in[3] * in[7] + in[1] * in[9])); - out[11] = 2 * (in[5] * in[6] + in[4] * in[7] + in[3] * in[8] + in[2] * in[9]); - out[12] = in[6] * in[6] - + 2 * (in[4] * in[8] + 2 * (in[5] * in[7] + in[3] * in[9])); - out[13] = 2 * (in[6] * in[7] + in[5] * in[8] + in[4] * in[9]); - out[14] = 2 * (in[7] * in[7] + in[6] * in[8] + 2 * in[5] * in[9]); - out[15] = 2 * (in[7] * in[8] + in[6] * in[9]); - out[16] = in[8] * in[8] + 4 * in[7] * in[9]; - out[17] = 2 * in[8] * in[9]; - out[18] = 2 * in[9] * in[9]; - } - - /** - * Returns in^2. - *

- * On entry: The |in| argument is in reduced coefficients form and |in[i]| < 2^27. - *

- * On exit: The |output| argument is in reduced coefficients form (indeed, one need only provide - * storage for 10 limbs) and |out[i]| < 2^26. - */ - static void square(long[] output, long[] in) { - long[] t = new long[19]; - squareInner(t, in); - // |t[i]| < 14*2^54 because the largest product of two limbs will be < 2^(27+27) and SquareInner - // adds together, at most, 14 of those products. - reduce(t, output); - } - - /** - * Takes a little-endian, 32-byte number and expands it into mixed radix form. - */ - static long[] expand(byte[] input) { - long[] output = new long[LIMB_CNT]; - for (int i = 0; i < LIMB_CNT; i++) { - output[i] = ((((long) (input[EXPAND_START[i]] & 0xff)) - | ((long) (input[EXPAND_START[i] + 1] & 0xff)) << 8 - | ((long) (input[EXPAND_START[i] + 2] & 0xff)) << 16 - | ((long) (input[EXPAND_START[i] + 3] & 0xff)) << 24) >> EXPAND_SHIFT[i]) & MASK[i & 1]; - } - return output; - } - - /** - * Takes a fully reduced mixed radix form number and contract it into a little-endian, 32-byte - * array. - *

- * On entry: |input_limbs[i]| < 2^26 - */ - @SuppressWarnings("NarrowingCompoundAssignment") - static byte[] contract(long[] inputLimbs) { - long[] input = Arrays.copyOf(inputLimbs, LIMB_CNT); - for (int j = 0; j < 2; j++) { - for (int i = 0; i < 9; i++) { - // This calculation is a time-invariant way to make input[i] non-negative by borrowing - // from the next-larger limb. - int carry = -(int) ((input[i] & (input[i] >> 31)) >> SHIFT[i & 1]); - input[i] = input[i] + (carry << SHIFT[i & 1]); - input[i + 1] -= carry; - } - - // There's no greater limb for input[9] to borrow from, but we can multiply by 19 and borrow - // from input[0], which is valid mod 2^255-19. - { - int carry = -(int) ((input[9] & (input[9] >> 31)) >> 25); - input[9] += (carry << 25); - input[0] -= (carry * 19); - } - - // After the first iteration, input[1..9] are non-negative and fit within 25 or 26 bits, - // depending on position. However, input[0] may be negative. - } - - // The first borrow-propagation pass above ended with every limb except (possibly) input[0] - // non-negative. - // - // If input[0] was negative after the first pass, then it was because of a carry from input[9]. - // On entry, input[9] < 2^26 so the carry was, at most, one, since (2**26-1) >> 25 = 1. Thus - // input[0] >= -19. - // - // In the second pass, each limb is decreased by at most one. Thus the second borrow-propagation - // pass could only have wrapped around to decrease input[0] again if the first pass left - // input[0] negative *and* input[1] through input[9] were all zero. In that case, input[1] is - // now 2^25 - 1, and this last borrow-propagation step will leave input[1] non-negative. - { - int carry = -(int) ((input[0] & (input[0] >> 31)) >> 26); - input[0] += (carry << 26); - input[1] -= carry; - } - - // All input[i] are now non-negative. However, there might be values between 2^25 and 2^26 in a - // limb which is, nominally, 25 bits wide. - for (int j = 0; j < 2; j++) { - for (int i = 0; i < 9; i++) { - int carry = (int) (input[i] >> SHIFT[i & 1]); - input[i] &= MASK[i & 1]; - input[i + 1] += carry; - } - } - - { - int carry = (int) (input[9] >> 25); - input[9] &= 0x1ffffff; - input[0] += 19 * carry; - } - - // If the first carry-chain pass, just above, ended up with a carry from input[9], and that - // caused input[0] to be out-of-bounds, then input[0] was < 2^26 + 2*19, because the carry was, - // at most, two. - // - // If the second pass carried from input[9] again then input[0] is < 2*19 and the input[9] -> - // input[0] carry didn't push input[0] out of bounds. - - // It still remains the case that input might be between 2^255-19 and 2^255. In this case, - // input[1..9] must take their maximum value and input[0] must be >= (2^255-19) & 0x3ffffff, - // which is 0x3ffffed. - int mask = gte((int) input[0], 0x3ffffed); - for (int i = 1; i < LIMB_CNT; i++) { - mask &= eq((int) input[i], MASK[i & 1]); - } - - // mask is either 0xffffffff (if input >= 2^255-19) and zero otherwise. Thus this conditionally - // subtracts 2^255-19. - input[0] -= mask & 0x3ffffed; - input[1] -= mask & 0x1ffffff; - for (int i = 2; i < LIMB_CNT; i += 2) { - input[i] -= mask & 0x3ffffff; - input[i + 1] -= mask & 0x1ffffff; - } - - for (int i = 0; i < LIMB_CNT; i++) { - input[i] <<= EXPAND_SHIFT[i]; - } - byte[] output = new byte[FIELD_LEN]; - for (int i = 0; i < LIMB_CNT; i++) { - output[EXPAND_START[i]] |= input[i] & 0xff; - output[EXPAND_START[i] + 1] |= (input[i] >> 8) & 0xff; - output[EXPAND_START[i] + 2] |= (input[i] >> 16) & 0xff; - output[EXPAND_START[i] + 3] |= (input[i] >> 24) & 0xff; - } - return output; - } - - /** - * Computes inverse of z = z(2^255 - 21) - *

- * Shamelessly copied from agl's code which was shamelessly copied from djb's code. Only the - * comment format and the variable namings are different from those. - */ - static void inverse(long[] out, long[] z) { - long[] z2 = new long[Field25519.LIMB_CNT]; - long[] z9 = new long[Field25519.LIMB_CNT]; - long[] z11 = new long[Field25519.LIMB_CNT]; - long[] z2To5Minus1 = new long[Field25519.LIMB_CNT]; - long[] z2To10Minus1 = new long[Field25519.LIMB_CNT]; - long[] z2To20Minus1 = new long[Field25519.LIMB_CNT]; - long[] z2To50Minus1 = new long[Field25519.LIMB_CNT]; - long[] z2To100Minus1 = new long[Field25519.LIMB_CNT]; - long[] t0 = new long[Field25519.LIMB_CNT]; - long[] t1 = new long[Field25519.LIMB_CNT]; - - square(z2, z); // 2 - square(t1, z2); // 4 - square(t0, t1); // 8 - mult(z9, t0, z); // 9 - mult(z11, z9, z2); // 11 - square(t0, z11); // 22 - mult(z2To5Minus1, t0, z9); // 2^5 - 2^0 = 31 - - square(t0, z2To5Minus1); // 2^6 - 2^1 - square(t1, t0); // 2^7 - 2^2 - square(t0, t1); // 2^8 - 2^3 - square(t1, t0); // 2^9 - 2^4 - square(t0, t1); // 2^10 - 2^5 - mult(z2To10Minus1, t0, z2To5Minus1); // 2^10 - 2^0 - - square(t0, z2To10Minus1); // 2^11 - 2^1 - square(t1, t0); // 2^12 - 2^2 - for (int i = 2; i < 10; i += 2) { // 2^20 - 2^10 - square(t0, t1); - square(t1, t0); - } - mult(z2To20Minus1, t1, z2To10Minus1); // 2^20 - 2^0 - - square(t0, z2To20Minus1); // 2^21 - 2^1 - square(t1, t0); // 2^22 - 2^2 - for (int i = 2; i < 20; i += 2) { // 2^40 - 2^20 - square(t0, t1); - square(t1, t0); - } - mult(t0, t1, z2To20Minus1); // 2^40 - 2^0 - - square(t1, t0); // 2^41 - 2^1 - square(t0, t1); // 2^42 - 2^2 - for (int i = 2; i < 10; i += 2) { // 2^50 - 2^10 - square(t1, t0); - square(t0, t1); - } - mult(z2To50Minus1, t0, z2To10Minus1); // 2^50 - 2^0 - - square(t0, z2To50Minus1); // 2^51 - 2^1 - square(t1, t0); // 2^52 - 2^2 - for (int i = 2; i < 50; i += 2) { // 2^100 - 2^50 - square(t0, t1); - square(t1, t0); - } - mult(z2To100Minus1, t1, z2To50Minus1); // 2^100 - 2^0 - - square(t1, z2To100Minus1); // 2^101 - 2^1 - square(t0, t1); // 2^102 - 2^2 - for (int i = 2; i < 100; i += 2) { // 2^200 - 2^100 - square(t1, t0); - square(t0, t1); - } - mult(t1, t0, z2To100Minus1); // 2^200 - 2^0 - - square(t0, t1); // 2^201 - 2^1 - square(t1, t0); // 2^202 - 2^2 - for (int i = 2; i < 50; i += 2) { // 2^250 - 2^50 - square(t0, t1); - square(t1, t0); - } - mult(t0, t1, z2To50Minus1); // 2^250 - 2^0 - - square(t1, t0); // 2^251 - 2^1 - square(t0, t1); // 2^252 - 2^2 - square(t1, t0); // 2^253 - 2^3 - square(t0, t1); // 2^254 - 2^4 - square(t1, t0); // 2^255 - 2^5 - mult(out, t1, z11); // 2^255 - 21 - } - - - /** - * Returns 0xffffffff iff a == b and zero otherwise. - */ - private static int eq(int a, int b) { - a = ~(a ^ b); - a &= a << 16; - a &= a << 8; - a &= a << 4; - a &= a << 2; - a &= a << 1; - return a >> 31; - } - - /** - * returns 0xffffffff if a >= b and zero otherwise, where a and b are both non-negative. - */ - private static int gte(int a, int b) { - a -= b; - // a >= 0 iff a >= b. - return ~(a >> 31); - } - } - - // (x = 0, y = 1) point - private static final CachedXYT CACHED_NEUTRAL = new CachedXYT( - new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); - private static final PartialXYZT NEUTRAL = new PartialXYZT( - new XYZ(new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}), - new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}); - - /** - * Projective point representation (X:Y:Z) satisfying x = X/Z, y = Y/Z - *

- * Note that this is referred as ge_p2 in ref10 impl. - * Also note that x = X, y = Y and z = Z below following Java coding style. - *

- * See - * Koyama K., Tsuruoka Y. (1993) Speeding up Elliptic Cryptosystems by Using a Signed Binary - * Window Method. - *

- * https://hyperelliptic.org/EFD/g1p/auto-twisted-projective.html - */ - private static class XYZ { - - final long[] x; - final long[] y; - final long[] z; - - XYZ() { - this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]); - } - - XYZ(long[] x, long[] y, long[] z) { - this.x = x; - this.y = y; - this.z = z; - } - - XYZ(XYZ xyz) { - x = Arrays.copyOf(xyz.x, Field25519.LIMB_CNT); - y = Arrays.copyOf(xyz.y, Field25519.LIMB_CNT); - z = Arrays.copyOf(xyz.z, Field25519.LIMB_CNT); - } - - XYZ(PartialXYZT partialXYZT) { - this(); - fromPartialXYZT(this, partialXYZT); - } - - /** - * ge_p1p1_to_p2.c - */ - static XYZ fromPartialXYZT(XYZ out, PartialXYZT in) { - Field25519.mult(out.x, in.xyz.x, in.t); - Field25519.mult(out.y, in.xyz.y, in.xyz.z); - Field25519.mult(out.z, in.xyz.z, in.t); - return out; - } - - /** - * Encodes this point to bytes. - */ - byte[] toBytes() { - long[] recip = new long[Field25519.LIMB_CNT]; - long[] x = new long[Field25519.LIMB_CNT]; - long[] y = new long[Field25519.LIMB_CNT]; - Field25519.inverse(recip, z); - Field25519.mult(x, this.x, recip); - Field25519.mult(y, this.y, recip); - byte[] s = Field25519.contract(y); - s[31] = (byte) (s[31] ^ (getLsb(x) << 7)); - return s; - } - - - /** - * Best effort fix-timing array comparison. - * - * @return true if two arrays are equal. - */ - private static boolean bytesEqual(final byte[] x, final byte[] y) { - if (x == null || y == null) { - return false; - } - if (x.length != y.length) { - return false; - } - int res = 0; - for (int i = 0; i < x.length; i++) { - res |= x[i] ^ y[i]; - } - return res == 0; - } - - /** - * Checks that the point is on curve - */ - boolean isOnCurve() { - long[] x2 = new long[Field25519.LIMB_CNT]; - Field25519.square(x2, x); - long[] y2 = new long[Field25519.LIMB_CNT]; - Field25519.square(y2, y); - long[] z2 = new long[Field25519.LIMB_CNT]; - Field25519.square(z2, z); - long[] z4 = new long[Field25519.LIMB_CNT]; - Field25519.square(z4, z2); - long[] lhs = new long[Field25519.LIMB_CNT]; - // lhs = y^2 - x^2 - Field25519.sub(lhs, y2, x2); - // lhs = z^2 * (y2 - x2) - Field25519.mult(lhs, lhs, z2); - long[] rhs = new long[Field25519.LIMB_CNT]; - // rhs = x^2 * y^2 - Field25519.mult(rhs, x2, y2); - // rhs = D * x^2 * y^2 - Field25519.mult(rhs, rhs, D); - // rhs = z^4 + D * x^2 * y^2 - Field25519.sum(rhs, z4); - // Field25519.mult reduces its output, but Field25519.sum does not, so we have to manually - // reduce it here. - Field25519.reduce(rhs, rhs); - // z^2 (y^2 - x^2) == z^4 + D * x^2 * y^2 - return bytesEqual(Field25519.contract(lhs), Field25519.contract(rhs)); - } - } - - /** - * Represents extended projective point representation (X:Y:Z:T) satisfying x = X/Z, y = Y/Z, - * XY = ZT - *

- * Note that this is referred as ge_p3 in ref10 impl. - * Also note that t = T below following Java coding style. - *

- * See - * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. - *

- * https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html - */ - private static class XYZT { - - final XYZ xyz; - final long[] t; - - XYZT() { - this(new XYZ(), new long[Field25519.LIMB_CNT]); - } - - XYZT(XYZ xyz, long[] t) { - this.xyz = xyz; - this.t = t; - } - - XYZT(PartialXYZT partialXYZT) { - this(); - fromPartialXYZT(this, partialXYZT); - } - - /** - * ge_p1p1_to_p2.c - */ - private static XYZT fromPartialXYZT(XYZT out, PartialXYZT in) { - Field25519.mult(out.xyz.x, in.xyz.x, in.t); - Field25519.mult(out.xyz.y, in.xyz.y, in.xyz.z); - Field25519.mult(out.xyz.z, in.xyz.z, in.t); - Field25519.mult(out.t, in.xyz.x, in.xyz.y); - return out; - } - - /** - * Decodes {@code s} into an extented projective point. - * See Section 5.1.3 Decoding in https://tools.ietf.org/html/rfc8032#section-5.1.3 - */ - private static XYZT fromBytesNegateVarTime(byte[] s) throws GeneralSecurityException { - long[] x = new long[Field25519.LIMB_CNT]; - long[] y = Field25519.expand(s); - long[] z = new long[Field25519.LIMB_CNT]; - z[0] = 1; - long[] t = new long[Field25519.LIMB_CNT]; - long[] u = new long[Field25519.LIMB_CNT]; - long[] v = new long[Field25519.LIMB_CNT]; - long[] vxx = new long[Field25519.LIMB_CNT]; - long[] check = new long[Field25519.LIMB_CNT]; - Field25519.square(u, y); - Field25519.mult(v, u, D); - Field25519.sub(u, u, z); // u = y^2 - 1 - Field25519.sum(v, v, z); // v = dy^2 + 1 - - long[] v3 = new long[Field25519.LIMB_CNT]; - Field25519.square(v3, v); - Field25519.mult(v3, v3, v); // v3 = v^3 - Field25519.square(x, v3); - Field25519.mult(x, x, v); - Field25519.mult(x, x, u); // x = uv^7 - - pow2252m3(x, x); // x = (uv^7)^((q-5)/8) - Field25519.mult(x, x, v3); - Field25519.mult(x, x, u); // x = uv^3(uv^7)^((q-5)/8) - - Field25519.square(vxx, x); - Field25519.mult(vxx, vxx, v); - Field25519.sub(check, vxx, u); // vx^2-u - if (isNonZeroVarTime(check)) { - Field25519.sum(check, vxx, u); // vx^2+u - if (isNonZeroVarTime(check)) { - throw new GeneralSecurityException("Cannot convert given bytes to extended projective " - + "coordinates. No square root exists for modulo 2^255-19"); - } - Field25519.mult(x, x, SQRTM1); - } - - if (!isNonZeroVarTime(x) && (s[31] & 0xff) >> 7 != 0) { - throw new GeneralSecurityException("Cannot convert given bytes to extended projective " - + "coordinates. Computed x is zero and encoded x's least significant bit is not zero"); - } - if (getLsb(x) == ((s[31] & 0xff) >> 7)) { - neg(x, x); - } - - Field25519.mult(t, x, y); - return new XYZT(new XYZ(x, y, z), t); - } - } - - /** - * Partial projective point representation ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T - *

- * Note that this is referred as complete form in the original ref10 impl (ge_p1p1). - * Also note that t = T below following Java coding style. - *

- * Although this has the same types as XYZT, it is redefined to have its own type so that it is - * readable and 1:1 corresponds to ref10 impl. - *

- * Can be converted to XYZT as follows: - * X1 = X * T = x * Z * T = x * Z1 - * Y1 = Y * Z = y * T * Z = y * Z1 - * Z1 = Z * T = Z * T - * T1 = X * Y = x * Z * y * T = x * y * Z1 = X1Y1 / Z1 - */ - private static class PartialXYZT { - - final XYZ xyz; - final long[] t; - - PartialXYZT() { - this(new XYZ(), new long[Field25519.LIMB_CNT]); - } - - PartialXYZT(XYZ xyz, long[] t) { - this.xyz = xyz; - this.t = t; - } - - PartialXYZT(PartialXYZT other) { - xyz = new XYZ(other.xyz); - t = Arrays.copyOf(other.t, Field25519.LIMB_CNT); - } - } - - /** - * Corresponds to the caching mentioned in the last paragraph of Section 3.1 of - * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. - * with Z = 1. - */ - private static class CachedXYT { - - final long[] yPlusX; - final long[] yMinusX; - final long[] t2d; - - /** - * Creates a cached XYZT with Z = 1 - * - * @param yPlusX y + x - * @param yMinusX y - x - * @param t2d 2d * xy - */ - CachedXYT(long[] yPlusX, long[] yMinusX, long[] t2d) { - this.yPlusX = yPlusX; - this.yMinusX = yMinusX; - this.t2d = t2d; - } - - CachedXYT(CachedXYT other) { - yPlusX = Arrays.copyOf(other.yPlusX, Field25519.LIMB_CNT); - yMinusX = Arrays.copyOf(other.yMinusX, Field25519.LIMB_CNT); - t2d = Arrays.copyOf(other.t2d, Field25519.LIMB_CNT); - } - - // z is one implicitly, so this just copies {@code in} to {@code output}. - void multByZ(long[] output, long[] in) { - System.arraycopy(in, 0, output, 0, Field25519.LIMB_CNT); - } - - /** - * If icopy is 1, copies {@code other} into this point. Time invariant wrt to icopy value. - */ - void copyConditional(CachedXYT other, int icopy) { - copyConditional(yPlusX, other.yPlusX, icopy); - copyConditional(yMinusX, other.yMinusX, icopy); - copyConditional(t2d, other.t2d, icopy); - } - - /** - * Conditionally copies a reduced-form limb arrays {@code b} into {@code a} if {@code icopy} is 1, - * but leave {@code a} unchanged if 'iswap' is 0. Runs in data-invariant time to avoid - * side-channel attacks. - * - *

NOTE that this function requires that {@code icopy} be 1 or 0; other values give wrong - * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the - * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must - * have magnitude less than Integer.MAX_VALUE. - */ - static void copyConditional(long[] a, long[] b, int icopy) { - int copy = -icopy; - for (int i = 0; i < Field25519.LIMB_CNT; i++) { - int x = copy & (((int) a[i]) ^ ((int) b[i])); - a[i] = ((int) a[i]) ^ x; - } - } - } - - private static class CachedXYZT extends CachedXYT { - - private final long[] z; - - CachedXYZT() { - this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]); - } - - /** - * ge_p3_to_cached.c - */ - CachedXYZT(XYZT xyzt) { - this(); - Field25519.sum(yPlusX, xyzt.xyz.y, xyzt.xyz.x); - Field25519.sub(yMinusX, xyzt.xyz.y, xyzt.xyz.x); - System.arraycopy(xyzt.xyz.z, 0, z, 0, Field25519.LIMB_CNT); - Field25519.mult(t2d, xyzt.t, D2); - } - - /** - * Creates a cached XYZT - * - * @param yPlusX Y + X - * @param yMinusX Y - X - * @param z Z - * @param t2d 2d * (XY/Z) - */ - CachedXYZT(long[] yPlusX, long[] yMinusX, long[] z, long[] t2d) { - super(yPlusX, yMinusX, t2d); - this.z = z; - } - - @Override - public void multByZ(long[] output, long[] in) { - Field25519.mult(output, in, z); - } - } - - /** - * Addition defined in Section 3.1 of - * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. - *

- * Please note that this is a partial of the operation listed there leaving out the final - * conversion from PartialXYZT to XYZT. - * - * @param extended extended projective point input - * @param cached cached projective point input - */ - private static void add(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) { - long[] t = new long[Field25519.LIMB_CNT]; - - // Y1 + X1 - Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x); - - // Y1 - X1 - Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x); - - // A = (Y1 - X1) * (Y2 - X2) - Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yMinusX); - - // B = (Y1 + X1) * (Y2 + X2) - Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yPlusX); - - // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper) - Field25519.mult(partialXYZT.t, extended.t, cached.t2d); - - // Z1 * Z2 - cached.multByZ(partialXYZT.xyz.x, extended.xyz.z); - - // D = 2 * Z1 * Z2 - Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x); - - // X3 = B - A - Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y); - - // Y3 = B + A - Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y); - - // Z3 = D + C - Field25519.sum(partialXYZT.xyz.z, t, partialXYZT.t); - - // T3 = D - C - Field25519.sub(partialXYZT.t, t, partialXYZT.t); - } - - /** - * Based on the addition defined in Section 3.1 of - * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. - *

- * Please note that this is a partial of the operation listed there leaving out the final - * conversion from PartialXYZT to XYZT. - * - * @param extended extended projective point input - * @param cached cached projective point input - */ - private static void sub(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) { - long[] t = new long[Field25519.LIMB_CNT]; - - // Y1 + X1 - Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x); - - // Y1 - X1 - Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x); - - // A = (Y1 - X1) * (Y2 + X2) - Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yPlusX); - - // B = (Y1 + X1) * (Y2 - X2) - Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yMinusX); - - // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper) - Field25519.mult(partialXYZT.t, extended.t, cached.t2d); - - // Z1 * Z2 - cached.multByZ(partialXYZT.xyz.x, extended.xyz.z); - - // D = 2 * Z1 * Z2 - Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x); - - // X3 = B - A - Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y); - - // Y3 = B + A - Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y); - - // Z3 = D - C - Field25519.sub(partialXYZT.xyz.z, t, partialXYZT.t); - - // T3 = D + C - Field25519.sum(partialXYZT.t, t, partialXYZT.t); - } - - /** - * Doubles {@code p} and puts the result into this PartialXYZT. - *

- * This is based on the addition defined in formula 7 in Section 3.3 of - * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited. - *

- * Please note that this is a partial of the operation listed there leaving out the final - * conversion from PartialXYZT to XYZT and also this fixes a typo in calculation of Y3 and T3 in - * the paper, H should be replaced with A+B. - */ - private static void doubleXYZ(PartialXYZT partialXYZT, XYZ p) { - long[] t0 = new long[Field25519.LIMB_CNT]; - - // XX = X1^2 - Field25519.square(partialXYZT.xyz.x, p.x); - - // YY = Y1^2 - Field25519.square(partialXYZT.xyz.z, p.y); - - // B' = Z1^2 - Field25519.square(partialXYZT.t, p.z); - - // B = 2 * B' - Field25519.sum(partialXYZT.t, partialXYZT.t, partialXYZT.t); - - // A = X1 + Y1 - Field25519.sum(partialXYZT.xyz.y, p.x, p.y); - - // AA = A^2 - Field25519.square(t0, partialXYZT.xyz.y); - - // Y3 = YY + XX - Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.x); - - // Z3 = YY - XX - Field25519.sub(partialXYZT.xyz.z, partialXYZT.xyz.z, partialXYZT.xyz.x); - - // X3 = AA - Y3 - Field25519.sub(partialXYZT.xyz.x, t0, partialXYZT.xyz.y); - - // T3 = B - Z3 - Field25519.sub(partialXYZT.t, partialXYZT.t, partialXYZT.xyz.z); - } - - /** - * Doubles {@code p} and puts the result into this PartialXYZT. - */ - private static void doubleXYZT(PartialXYZT partialXYZT, XYZT p) { - doubleXYZ(partialXYZT, p.xyz); - } - - /** - * Compares two byte values in constant time. - */ - private static int eq(int a, int b) { - int r = ~(a ^ b) & 0xff; - r &= r << 4; - r &= r << 2; - r &= r << 1; - return (r >> 7) & 1; - } - - /** - * This is a constant time operation where point b*B*256^pos is stored in {@code t}. - * When b is 0, t remains the same (i.e., neutral point). - *

- * Although B_TABLE[32][8] (B_TABLE[i][j] = (j+1)*B*256^i) has j values in [0, 7], the select - * method negates the corresponding point if b is negative (which is straight forward in elliptic - * curves by just negating y coordinate). Therefore we can get multiples of B with the half of - * memory requirements. - * - * @param t neutral element (i.e., point 0), also serves as output. - * @param pos in B[pos][j] = (j+1)*B*256^pos - * @param b value in [-8, 8] range. - */ - private static void select(CachedXYT t, int pos, byte b) { - int bnegative = (b & 0xff) >> 7; - int babs = b - (((-bnegative) & b) << 1); - - t.copyConditional(B_TABLE[pos][0], eq(babs, 1)); - t.copyConditional(B_TABLE[pos][1], eq(babs, 2)); - t.copyConditional(B_TABLE[pos][2], eq(babs, 3)); - t.copyConditional(B_TABLE[pos][3], eq(babs, 4)); - t.copyConditional(B_TABLE[pos][4], eq(babs, 5)); - t.copyConditional(B_TABLE[pos][5], eq(babs, 6)); - t.copyConditional(B_TABLE[pos][6], eq(babs, 7)); - t.copyConditional(B_TABLE[pos][7], eq(babs, 8)); - - long[] yPlusX = Arrays.copyOf(t.yMinusX, Field25519.LIMB_CNT); - long[] yMinusX = Arrays.copyOf(t.yPlusX, Field25519.LIMB_CNT); - long[] t2d = Arrays.copyOf(t.t2d, Field25519.LIMB_CNT); - neg(t2d, t2d); - CachedXYT minust = new CachedXYT(yPlusX, yMinusX, t2d); - t.copyConditional(minust, bnegative); - } - - /** - * Computes {@code a}*B - * where a = a[0]+256*a[1]+...+256^31 a[31] and - * B is the Ed25519 base point (x,4/5) with x positive. - *

- * Preconditions: - * a[31] <= 127 - * - * @throws IllegalStateException iff there is arithmetic error. - */ - @SuppressWarnings("NarrowingCompoundAssignment") - private static XYZ scalarMultWithBase(byte[] a) { - byte[] e = new byte[2 * Field25519.FIELD_LEN]; - for (int i = 0; i < Field25519.FIELD_LEN; i++) { - e[2 * i + 0] = (byte) (((a[i] & 0xff) >> 0) & 0xf); - e[2 * i + 1] = (byte) (((a[i] & 0xff) >> 4) & 0xf); - } - // each e[i] is between 0 and 15 - // e[63] is between 0 and 7 - - // Rewrite e in a way that each e[i] is in [-8, 8]. - // This can be done since a[63] is in [0, 7], the carry-over onto the most significant byte - // a[63] can be at most 1. - int carry = 0; - for (int i = 0; i < e.length - 1; i++) { - e[i] += carry; - carry = e[i] + 8; - carry >>= 4; - e[i] -= carry << 4; - } - e[e.length - 1] += carry; - - PartialXYZT ret = new PartialXYZT(NEUTRAL); - XYZT xyzt = new XYZT(); - // Although B_TABLE's i can be at most 31 (stores only 32 4bit multiples of B) and we have 64 - // 4bit values in e array, the below for loop adds cached values by iterating e by two in odd - // indices. After the result, we can double the result point 4 times to shift the multiplication - // scalar by 4 bits. - for (int i = 1; i < e.length; i += 2) { - CachedXYT t = new CachedXYT(CACHED_NEUTRAL); - select(t, i / 2, e[i]); - add(ret, XYZT.fromPartialXYZT(xyzt, ret), t); - } - - // Doubles the result 4 times to shift the multiplication scalar 4 bits to get the actual result - // for the odd indices in e. - XYZ xyz = new XYZ(); - doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); - doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); - doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); - doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret)); - - // Add multiples of B for even indices of e. - for (int i = 0; i < e.length; i += 2) { - CachedXYT t = new CachedXYT(CACHED_NEUTRAL); - select(t, i / 2, e[i]); - add(ret, XYZT.fromPartialXYZT(xyzt, ret), t); - } - - // This check is to protect against flaws, i.e. if there is a computation error through a - // faulty CPU or if the implementation contains a bug. - XYZ result = new XYZ(ret); - if (!result.isOnCurve()) { - throw new IllegalStateException("arithmetic error in scalar multiplication"); - } - return result; - } - - @SuppressWarnings("NarrowingCompoundAssignment") - private static byte[] slide(byte[] a) { - byte[] r = new byte[256]; - // Writes each bit in a[0..31] into r[0..255]: - // a = a[0]+256*a[1]+...+256^31*a[31] is equal to - // r = r[0]+2*r[1]+...+2^255*r[255] - for (int i = 0; i < 256; i++) { - r[i] = (byte) (1 & ((a[i >> 3] & 0xff) >> (i & 7))); - } - - // Transforms r[i] as odd values in [-15, 15] - for (int i = 0; i < 256; i++) { - if (r[i] != 0) { - for (int b = 1; b <= 6 && i + b < 256; b++) { - if (r[i + b] != 0) { - if (r[i] + (r[i + b] << b) <= 15) { - r[i] += r[i + b] << b; - r[i + b] = 0; - } else if (r[i] - (r[i + b] << b) >= -15) { - r[i] -= r[i + b] << b; - for (int k = i + b; k < 256; k++) { - if (r[k] == 0) { - r[k] = 1; - break; - } - r[k] = 0; - } - } else { - break; - } - } - } - } - } - return r; - } - - /** - * Computes {@code a}*{@code pointA}+{@code b}*B - * where a = a[0]+256*a[1]+...+256^31*a[31]. - * and b = b[0]+256*b[1]+...+256^31*b[31]. - * B is the Ed25519 base point (x,4/5) with x positive. - *

- * Note that execution time varies based on the input since this will only be used in verification - * of signatures. - */ - private static XYZ doubleScalarMultVarTime(byte[] a, XYZT pointA, byte[] b) { - // pointA, 3*pointA, 5*pointA, 7*pointA, 9*pointA, 11*pointA, 13*pointA, 15*pointA - CachedXYZT[] pointAArray = new CachedXYZT[8]; - pointAArray[0] = new CachedXYZT(pointA); - PartialXYZT t = new PartialXYZT(); - doubleXYZT(t, pointA); - XYZT doubleA = new XYZT(t); - for (int i = 1; i < pointAArray.length; i++) { - add(t, doubleA, pointAArray[i - 1]); - pointAArray[i] = new CachedXYZT(new XYZT(t)); - } - - byte[] aSlide = slide(a); - byte[] bSlide = slide(b); - t = new PartialXYZT(NEUTRAL); - XYZT u = new XYZT(); - int i = 255; - for (; i >= 0; i--) { - if (aSlide[i] != 0 || bSlide[i] != 0) { - break; - } - } - for (; i >= 0; i--) { - doubleXYZ(t, new XYZ(t)); - if (aSlide[i] > 0) { - add(t, XYZT.fromPartialXYZT(u, t), pointAArray[aSlide[i] / 2]); - } else if (aSlide[i] < 0) { - sub(t, XYZT.fromPartialXYZT(u, t), pointAArray[-aSlide[i] / 2]); - } - if (bSlide[i] > 0) { - add(t, XYZT.fromPartialXYZT(u, t), B2[bSlide[i] / 2]); - } else if (bSlide[i] < 0) { - sub(t, XYZT.fromPartialXYZT(u, t), B2[-bSlide[i] / 2]); - } - } - - return new XYZ(t); - } - - /** - * Returns true if {@code in} is nonzero. - *

- * Note that execution time might depend on the input {@code in}. - */ - private static boolean isNonZeroVarTime(long[] in) { - long[] inCopy = new long[in.length + 1]; - System.arraycopy(in, 0, inCopy, 0, in.length); - Field25519.reduceCoefficients(inCopy); - byte[] bytes = Field25519.contract(inCopy); - for (byte b : bytes) { - if (b != 0) { - return true; - } - } - return false; - } - - /** - * Returns the least significant bit of {@code in}. - */ - private static int getLsb(long[] in) { - return Field25519.contract(in)[0] & 1; - } - - /** - * Negates all values in {@code in} and store it in {@code out}. - */ - private static void neg(long[] out, long[] in) { - for (int i = 0; i < in.length; i++) { - out[i] = -in[i]; - } - } - - /** - * Computes {@code in}^(2^252-3) mod 2^255-19 and puts the result in {@code out}. - */ - private static void pow2252m3(long[] out, long[] in) { - long[] t0 = new long[Field25519.LIMB_CNT]; - long[] t1 = new long[Field25519.LIMB_CNT]; - long[] t2 = new long[Field25519.LIMB_CNT]; - - // z2 = z1^2^1 - Field25519.square(t0, in); - - // z8 = z2^2^2 - Field25519.square(t1, t0); - for (int i = 1; i < 2; i++) { - Field25519.square(t1, t1); - } - - // z9 = z1*z8 - Field25519.mult(t1, in, t1); - - // z11 = z2*z9 - Field25519.mult(t0, t0, t1); - - // z22 = z11^2^1 - Field25519.square(t0, t0); - - // z_5_0 = z9*z22 - Field25519.mult(t0, t1, t0); - - // z_10_5 = z_5_0^2^5 - Field25519.square(t1, t0); - for (int i = 1; i < 5; i++) { - Field25519.square(t1, t1); - } - - // z_10_0 = z_10_5*z_5_0 - Field25519.mult(t0, t1, t0); - - // z_20_10 = z_10_0^2^10 - Field25519.square(t1, t0); - for (int i = 1; i < 10; i++) { - Field25519.square(t1, t1); - } - - // z_20_0 = z_20_10*z_10_0 - Field25519.mult(t1, t1, t0); - - // z_40_20 = z_20_0^2^20 - Field25519.square(t2, t1); - for (int i = 1; i < 20; i++) { - Field25519.square(t2, t2); - } - - // z_40_0 = z_40_20*z_20_0 - Field25519.mult(t1, t2, t1); - - // z_50_10 = z_40_0^2^10 - Field25519.square(t1, t1); - for (int i = 1; i < 10; i++) { - Field25519.square(t1, t1); - } - - // z_50_0 = z_50_10*z_10_0 - Field25519.mult(t0, t1, t0); - - // z_100_50 = z_50_0^2^50 - Field25519.square(t1, t0); - for (int i = 1; i < 50; i++) { - Field25519.square(t1, t1); - } - - // z_100_0 = z_100_50*z_50_0 - Field25519.mult(t1, t1, t0); - - // z_200_100 = z_100_0^2^100 - Field25519.square(t2, t1); - for (int i = 1; i < 100; i++) { - Field25519.square(t2, t2); - } - - // z_200_0 = z_200_100*z_100_0 - Field25519.mult(t1, t2, t1); - - // z_250_50 = z_200_0^2^50 - Field25519.square(t1, t1); - for (int i = 1; i < 50; i++) { - Field25519.square(t1, t1); - } - - // z_250_0 = z_250_50*z_50_0 - Field25519.mult(t0, t1, t0); - - // z_252_2 = z_250_0^2^2 - Field25519.square(t0, t0); - for (int i = 1; i < 2; i++) { - Field25519.square(t0, t0); - } - - // z_252_3 = z_252_2*z1 - Field25519.mult(out, t0, in); - } - - /** - * Returns 3 bytes of {@code in} starting from {@code idx} in Little-Endian format. - */ - private static long load3(byte[] in, int idx) { - long result; - result = (long) in[idx] & 0xff; - result |= (long) (in[idx + 1] & 0xff) << 8; - result |= (long) (in[idx + 2] & 0xff) << 16; - return result; - } - - /** - * Returns 4 bytes of {@code in} starting from {@code idx} in Little-Endian format. - */ - private static long load4(byte[] in, int idx) { - long result = load3(in, idx); - result |= (long) (in[idx + 3] & 0xff) << 24; - return result; - } - - /** - * Input: - * s[0]+256*s[1]+...+256^63*s[63] = s - *

- * Output: - * s[0]+256*s[1]+...+256^31*s[31] = s mod l - * where l = 2^252 + 27742317777372353535851937790883648493. - * Overwrites s in place. - */ - private static void reduce(byte[] s) { - // Observation: - // 2^252 mod l is equivalent to -27742317777372353535851937790883648493 mod l - // Let m = -27742317777372353535851937790883648493 - // Thus a*2^252+b mod l is equivalent to a*m+b mod l - // - // First s is divided into chunks of 21 bits as follows: - // s0+2^21*s1+2^42*s3+...+2^462*s23 = s[0]+256*s[1]+...+256^63*s[63] - long s0 = 2097151 & load3(s, 0); - long s1 = 2097151 & (load4(s, 2) >> 5); - long s2 = 2097151 & (load3(s, 5) >> 2); - long s3 = 2097151 & (load4(s, 7) >> 7); - long s4 = 2097151 & (load4(s, 10) >> 4); - long s5 = 2097151 & (load3(s, 13) >> 1); - long s6 = 2097151 & (load4(s, 15) >> 6); - long s7 = 2097151 & (load3(s, 18) >> 3); - long s8 = 2097151 & load3(s, 21); - long s9 = 2097151 & (load4(s, 23) >> 5); - long s10 = 2097151 & (load3(s, 26) >> 2); - long s11 = 2097151 & (load4(s, 28) >> 7); - long s12 = 2097151 & (load4(s, 31) >> 4); - long s13 = 2097151 & (load3(s, 34) >> 1); - long s14 = 2097151 & (load4(s, 36) >> 6); - long s15 = 2097151 & (load3(s, 39) >> 3); - long s16 = 2097151 & load3(s, 42); - long s17 = 2097151 & (load4(s, 44) >> 5); - long s18 = 2097151 & (load3(s, 47) >> 2); - long s19 = 2097151 & (load4(s, 49) >> 7); - long s20 = 2097151 & (load4(s, 52) >> 4); - long s21 = 2097151 & (load3(s, 55) >> 1); - long s22 = 2097151 & (load4(s, 57) >> 6); - long s23 = (load4(s, 60) >> 3); - long carry0; - long carry1; - long carry2; - long carry3; - long carry4; - long carry5; - long carry6; - long carry7; - long carry8; - long carry9; - long carry10; - long carry11; - long carry12; - long carry13; - long carry14; - long carry15; - long carry16; - - // s23*2^462 = s23*2^210*2^252 is equivalent to s23*2^210*m in mod l - // As m is a 125 bit number, the result needs to scattered to 6 limbs (125/21 ceil is 6) - // starting from s11 (s11*2^210) - // m = [666643, 470296, 654183, -997805, 136657, -683901] in 21-bit limbs - s11 += s23 * 666643; - s12 += s23 * 470296; - s13 += s23 * 654183; - s14 -= s23 * 997805; - s15 += s23 * 136657; - s16 -= s23 * 683901; - // s23 = 0; - - s10 += s22 * 666643; - s11 += s22 * 470296; - s12 += s22 * 654183; - s13 -= s22 * 997805; - s14 += s22 * 136657; - s15 -= s22 * 683901; - // s22 = 0; - - s9 += s21 * 666643; - s10 += s21 * 470296; - s11 += s21 * 654183; - s12 -= s21 * 997805; - s13 += s21 * 136657; - s14 -= s21 * 683901; - // s21 = 0; - - s8 += s20 * 666643; - s9 += s20 * 470296; - s10 += s20 * 654183; - s11 -= s20 * 997805; - s12 += s20 * 136657; - s13 -= s20 * 683901; - // s20 = 0; - - s7 += s19 * 666643; - s8 += s19 * 470296; - s9 += s19 * 654183; - s10 -= s19 * 997805; - s11 += s19 * 136657; - s12 -= s19 * 683901; - // s19 = 0; - - s6 += s18 * 666643; - s7 += s18 * 470296; - s8 += s18 * 654183; - s9 -= s18 * 997805; - s10 += s18 * 136657; - s11 -= s18 * 683901; - // s18 = 0; - - // Reduce the bit length of limbs from s6 to s15 to 21-bits. - carry6 = (s6 + (1 << 20)) >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry8 = (s8 + (1 << 20)) >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry10 = (s10 + (1 << 20)) >> 21; - s11 += carry10; - s10 -= carry10 << 21; - carry12 = (s12 + (1 << 20)) >> 21; - s13 += carry12; - s12 -= carry12 << 21; - carry14 = (s14 + (1 << 20)) >> 21; - s15 += carry14; - s14 -= carry14 << 21; - carry16 = (s16 + (1 << 20)) >> 21; - s17 += carry16; - s16 -= carry16 << 21; - - carry7 = (s7 + (1 << 20)) >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry9 = (s9 + (1 << 20)) >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry11 = (s11 + (1 << 20)) >> 21; - s12 += carry11; - s11 -= carry11 << 21; - carry13 = (s13 + (1 << 20)) >> 21; - s14 += carry13; - s13 -= carry13 << 21; - carry15 = (s15 + (1 << 20)) >> 21; - s16 += carry15; - s15 -= carry15 << 21; - - // Resume reduction where we left off. - s5 += s17 * 666643; - s6 += s17 * 470296; - s7 += s17 * 654183; - s8 -= s17 * 997805; - s9 += s17 * 136657; - s10 -= s17 * 683901; - // s17 = 0; - - s4 += s16 * 666643; - s5 += s16 * 470296; - s6 += s16 * 654183; - s7 -= s16 * 997805; - s8 += s16 * 136657; - s9 -= s16 * 683901; - // s16 = 0; - - s3 += s15 * 666643; - s4 += s15 * 470296; - s5 += s15 * 654183; - s6 -= s15 * 997805; - s7 += s15 * 136657; - s8 -= s15 * 683901; - // s15 = 0; - - s2 += s14 * 666643; - s3 += s14 * 470296; - s4 += s14 * 654183; - s5 -= s14 * 997805; - s6 += s14 * 136657; - s7 -= s14 * 683901; - // s14 = 0; - - s1 += s13 * 666643; - s2 += s13 * 470296; - s3 += s13 * 654183; - s4 -= s13 * 997805; - s5 += s13 * 136657; - s6 -= s13 * 683901; - // s13 = 0; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - // Reduce the range of limbs from s0 to s11 to 21-bits. - carry0 = (s0 + (1 << 20)) >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry2 = (s2 + (1 << 20)) >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry4 = (s4 + (1 << 20)) >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry6 = (s6 + (1 << 20)) >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry8 = (s8 + (1 << 20)) >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry10 = (s10 + (1 << 20)) >> 21; - s11 += carry10; - s10 -= carry10 << 21; - - carry1 = (s1 + (1 << 20)) >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry3 = (s3 + (1 << 20)) >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry5 = (s5 + (1 << 20)) >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry7 = (s7 + (1 << 20)) >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry9 = (s9 + (1 << 20)) >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry11 = (s11 + (1 << 20)) >> 21; - s12 += carry11; - s11 -= carry11 << 21; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - // Carry chain reduction to propagate excess bits from s0 to s5 to the most significant limbs. - carry0 = s0 >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry1 = s1 >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry2 = s2 >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry3 = s3 >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry4 = s4 >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry5 = s5 >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry6 = s6 >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry7 = s7 >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry8 = s8 >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry9 = s9 >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry10 = s10 >> 21; - s11 += carry10; - s10 -= carry10 << 21; - carry11 = s11 >> 21; - s12 += carry11; - s11 -= carry11 << 21; - - // Do one last reduction as s12 might be 1. - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - // s12 = 0; - - carry0 = s0 >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry1 = s1 >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry2 = s2 >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry3 = s3 >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry4 = s4 >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry5 = s5 >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry6 = s6 >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry7 = s7 >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry8 = s8 >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry9 = s9 >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry10 = s10 >> 21; - s11 += carry10; - s10 -= carry10 << 21; - - // Serialize the result into the s. - s[0] = (byte) s0; - s[1] = (byte) (s0 >> 8); - s[2] = (byte) ((s0 >> 16) | (s1 << 5)); - s[3] = (byte) (s1 >> 3); - s[4] = (byte) (s1 >> 11); - s[5] = (byte) ((s1 >> 19) | (s2 << 2)); - s[6] = (byte) (s2 >> 6); - s[7] = (byte) ((s2 >> 14) | (s3 << 7)); - s[8] = (byte) (s3 >> 1); - s[9] = (byte) (s3 >> 9); - s[10] = (byte) ((s3 >> 17) | (s4 << 4)); - s[11] = (byte) (s4 >> 4); - s[12] = (byte) (s4 >> 12); - s[13] = (byte) ((s4 >> 20) | (s5 << 1)); - s[14] = (byte) (s5 >> 7); - s[15] = (byte) ((s5 >> 15) | (s6 << 6)); - s[16] = (byte) (s6 >> 2); - s[17] = (byte) (s6 >> 10); - s[18] = (byte) ((s6 >> 18) | (s7 << 3)); - s[19] = (byte) (s7 >> 5); - s[20] = (byte) (s7 >> 13); - s[21] = (byte) s8; - s[22] = (byte) (s8 >> 8); - s[23] = (byte) ((s8 >> 16) | (s9 << 5)); - s[24] = (byte) (s9 >> 3); - s[25] = (byte) (s9 >> 11); - s[26] = (byte) ((s9 >> 19) | (s10 << 2)); - s[27] = (byte) (s10 >> 6); - s[28] = (byte) ((s10 >> 14) | (s11 << 7)); - s[29] = (byte) (s11 >> 1); - s[30] = (byte) (s11 >> 9); - s[31] = (byte) (s11 >> 17); - } - - /** - * Input: - * a[0]+256*a[1]+...+256^31*a[31] = a - * b[0]+256*b[1]+...+256^31*b[31] = b - * c[0]+256*c[1]+...+256^31*c[31] = c - *

- * Output: - * s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l - * where l = 2^252 + 27742317777372353535851937790883648493. - */ - private static void mulAdd(byte[] s, byte[] a, byte[] b, byte[] c) { - // This is very similar to Ed25519.reduce, the difference in here is that it computes ab+c - // See Ed25519.reduce for related comments. - long a0 = 2097151 & load3(a, 0); - long a1 = 2097151 & (load4(a, 2) >> 5); - long a2 = 2097151 & (load3(a, 5) >> 2); - long a3 = 2097151 & (load4(a, 7) >> 7); - long a4 = 2097151 & (load4(a, 10) >> 4); - long a5 = 2097151 & (load3(a, 13) >> 1); - long a6 = 2097151 & (load4(a, 15) >> 6); - long a7 = 2097151 & (load3(a, 18) >> 3); - long a8 = 2097151 & load3(a, 21); - long a9 = 2097151 & (load4(a, 23) >> 5); - long a10 = 2097151 & (load3(a, 26) >> 2); - long a11 = (load4(a, 28) >> 7); - long b0 = 2097151 & load3(b, 0); - long b1 = 2097151 & (load4(b, 2) >> 5); - long b2 = 2097151 & (load3(b, 5) >> 2); - long b3 = 2097151 & (load4(b, 7) >> 7); - long b4 = 2097151 & (load4(b, 10) >> 4); - long b5 = 2097151 & (load3(b, 13) >> 1); - long b6 = 2097151 & (load4(b, 15) >> 6); - long b7 = 2097151 & (load3(b, 18) >> 3); - long b8 = 2097151 & load3(b, 21); - long b9 = 2097151 & (load4(b, 23) >> 5); - long b10 = 2097151 & (load3(b, 26) >> 2); - long b11 = (load4(b, 28) >> 7); - long c0 = 2097151 & load3(c, 0); - long c1 = 2097151 & (load4(c, 2) >> 5); - long c2 = 2097151 & (load3(c, 5) >> 2); - long c3 = 2097151 & (load4(c, 7) >> 7); - long c4 = 2097151 & (load4(c, 10) >> 4); - long c5 = 2097151 & (load3(c, 13) >> 1); - long c6 = 2097151 & (load4(c, 15) >> 6); - long c7 = 2097151 & (load3(c, 18) >> 3); - long c8 = 2097151 & load3(c, 21); - long c9 = 2097151 & (load4(c, 23) >> 5); - long c10 = 2097151 & (load3(c, 26) >> 2); - long c11 = (load4(c, 28) >> 7); - long s0; - long s1; - long s2; - long s3; - long s4; - long s5; - long s6; - long s7; - long s8; - long s9; - long s10; - long s11; - long s12; - long s13; - long s14; - long s15; - long s16; - long s17; - long s18; - long s19; - long s20; - long s21; - long s22; - long s23; - long carry0; - long carry1; - long carry2; - long carry3; - long carry4; - long carry5; - long carry6; - long carry7; - long carry8; - long carry9; - long carry10; - long carry11; - long carry12; - long carry13; - long carry14; - long carry15; - long carry16; - long carry17; - long carry18; - long carry19; - long carry20; - long carry21; - long carry22; - - s0 = c0 + a0 * b0; - s1 = c1 + a0 * b1 + a1 * b0; - s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; - s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; - s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; - s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; - s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; - s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0; - s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 - + a8 * b0; - s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 - + a8 * b1 + a9 * b0; - s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 - + a8 * b2 + a9 * b1 + a10 * b0; - s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 - + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; - s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3 - + a10 * b2 + a11 * b1; - s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3 - + a11 * b2; - s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 - + a11 * b3; - s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4; - s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; - s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; - s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; - s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; - s20 = a9 * b11 + a10 * b10 + a11 * b9; - s21 = a10 * b11 + a11 * b10; - s22 = a11 * b11; - s23 = 0; - - carry0 = (s0 + (1 << 20)) >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry2 = (s2 + (1 << 20)) >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry4 = (s4 + (1 << 20)) >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry6 = (s6 + (1 << 20)) >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry8 = (s8 + (1 << 20)) >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry10 = (s10 + (1 << 20)) >> 21; - s11 += carry10; - s10 -= carry10 << 21; - carry12 = (s12 + (1 << 20)) >> 21; - s13 += carry12; - s12 -= carry12 << 21; - carry14 = (s14 + (1 << 20)) >> 21; - s15 += carry14; - s14 -= carry14 << 21; - carry16 = (s16 + (1 << 20)) >> 21; - s17 += carry16; - s16 -= carry16 << 21; - carry18 = (s18 + (1 << 20)) >> 21; - s19 += carry18; - s18 -= carry18 << 21; - carry20 = (s20 + (1 << 20)) >> 21; - s21 += carry20; - s20 -= carry20 << 21; - carry22 = (s22 + (1 << 20)) >> 21; - s23 += carry22; - s22 -= carry22 << 21; - - carry1 = (s1 + (1 << 20)) >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry3 = (s3 + (1 << 20)) >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry5 = (s5 + (1 << 20)) >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry7 = (s7 + (1 << 20)) >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry9 = (s9 + (1 << 20)) >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry11 = (s11 + (1 << 20)) >> 21; - s12 += carry11; - s11 -= carry11 << 21; - carry13 = (s13 + (1 << 20)) >> 21; - s14 += carry13; - s13 -= carry13 << 21; - carry15 = (s15 + (1 << 20)) >> 21; - s16 += carry15; - s15 -= carry15 << 21; - carry17 = (s17 + (1 << 20)) >> 21; - s18 += carry17; - s17 -= carry17 << 21; - carry19 = (s19 + (1 << 20)) >> 21; - s20 += carry19; - s19 -= carry19 << 21; - carry21 = (s21 + (1 << 20)) >> 21; - s22 += carry21; - s21 -= carry21 << 21; - - s11 += s23 * 666643; - s12 += s23 * 470296; - s13 += s23 * 654183; - s14 -= s23 * 997805; - s15 += s23 * 136657; - s16 -= s23 * 683901; - // s23 = 0; - - s10 += s22 * 666643; - s11 += s22 * 470296; - s12 += s22 * 654183; - s13 -= s22 * 997805; - s14 += s22 * 136657; - s15 -= s22 * 683901; - // s22 = 0; - - s9 += s21 * 666643; - s10 += s21 * 470296; - s11 += s21 * 654183; - s12 -= s21 * 997805; - s13 += s21 * 136657; - s14 -= s21 * 683901; - // s21 = 0; - - s8 += s20 * 666643; - s9 += s20 * 470296; - s10 += s20 * 654183; - s11 -= s20 * 997805; - s12 += s20 * 136657; - s13 -= s20 * 683901; - // s20 = 0; - - s7 += s19 * 666643; - s8 += s19 * 470296; - s9 += s19 * 654183; - s10 -= s19 * 997805; - s11 += s19 * 136657; - s12 -= s19 * 683901; - // s19 = 0; - - s6 += s18 * 666643; - s7 += s18 * 470296; - s8 += s18 * 654183; - s9 -= s18 * 997805; - s10 += s18 * 136657; - s11 -= s18 * 683901; - // s18 = 0; - - carry6 = (s6 + (1 << 20)) >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry8 = (s8 + (1 << 20)) >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry10 = (s10 + (1 << 20)) >> 21; - s11 += carry10; - s10 -= carry10 << 21; - carry12 = (s12 + (1 << 20)) >> 21; - s13 += carry12; - s12 -= carry12 << 21; - carry14 = (s14 + (1 << 20)) >> 21; - s15 += carry14; - s14 -= carry14 << 21; - carry16 = (s16 + (1 << 20)) >> 21; - s17 += carry16; - s16 -= carry16 << 21; - - carry7 = (s7 + (1 << 20)) >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry9 = (s9 + (1 << 20)) >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry11 = (s11 + (1 << 20)) >> 21; - s12 += carry11; - s11 -= carry11 << 21; - carry13 = (s13 + (1 << 20)) >> 21; - s14 += carry13; - s13 -= carry13 << 21; - carry15 = (s15 + (1 << 20)) >> 21; - s16 += carry15; - s15 -= carry15 << 21; - - s5 += s17 * 666643; - s6 += s17 * 470296; - s7 += s17 * 654183; - s8 -= s17 * 997805; - s9 += s17 * 136657; - s10 -= s17 * 683901; - // s17 = 0; - - s4 += s16 * 666643; - s5 += s16 * 470296; - s6 += s16 * 654183; - s7 -= s16 * 997805; - s8 += s16 * 136657; - s9 -= s16 * 683901; - // s16 = 0; - - s3 += s15 * 666643; - s4 += s15 * 470296; - s5 += s15 * 654183; - s6 -= s15 * 997805; - s7 += s15 * 136657; - s8 -= s15 * 683901; - // s15 = 0; - - s2 += s14 * 666643; - s3 += s14 * 470296; - s4 += s14 * 654183; - s5 -= s14 * 997805; - s6 += s14 * 136657; - s7 -= s14 * 683901; - // s14 = 0; - - s1 += s13 * 666643; - s2 += s13 * 470296; - s3 += s13 * 654183; - s4 -= s13 * 997805; - s5 += s13 * 136657; - s6 -= s13 * 683901; - // s13 = 0; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - carry0 = (s0 + (1 << 20)) >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry2 = (s2 + (1 << 20)) >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry4 = (s4 + (1 << 20)) >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry6 = (s6 + (1 << 20)) >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry8 = (s8 + (1 << 20)) >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry10 = (s10 + (1 << 20)) >> 21; - s11 += carry10; - s10 -= carry10 << 21; - - carry1 = (s1 + (1 << 20)) >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry3 = (s3 + (1 << 20)) >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry5 = (s5 + (1 << 20)) >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry7 = (s7 + (1 << 20)) >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry9 = (s9 + (1 << 20)) >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry11 = (s11 + (1 << 20)) >> 21; - s12 += carry11; - s11 -= carry11 << 21; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - s12 = 0; - - carry0 = s0 >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry1 = s1 >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry2 = s2 >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry3 = s3 >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry4 = s4 >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry5 = s5 >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry6 = s6 >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry7 = s7 >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry8 = s8 >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry9 = s9 >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry10 = s10 >> 21; - s11 += carry10; - s10 -= carry10 << 21; - carry11 = s11 >> 21; - s12 += carry11; - s11 -= carry11 << 21; - - s0 += s12 * 666643; - s1 += s12 * 470296; - s2 += s12 * 654183; - s3 -= s12 * 997805; - s4 += s12 * 136657; - s5 -= s12 * 683901; - // s12 = 0; - - carry0 = s0 >> 21; - s1 += carry0; - s0 -= carry0 << 21; - carry1 = s1 >> 21; - s2 += carry1; - s1 -= carry1 << 21; - carry2 = s2 >> 21; - s3 += carry2; - s2 -= carry2 << 21; - carry3 = s3 >> 21; - s4 += carry3; - s3 -= carry3 << 21; - carry4 = s4 >> 21; - s5 += carry4; - s4 -= carry4 << 21; - carry5 = s5 >> 21; - s6 += carry5; - s5 -= carry5 << 21; - carry6 = s6 >> 21; - s7 += carry6; - s6 -= carry6 << 21; - carry7 = s7 >> 21; - s8 += carry7; - s7 -= carry7 << 21; - carry8 = s8 >> 21; - s9 += carry8; - s8 -= carry8 << 21; - carry9 = s9 >> 21; - s10 += carry9; - s9 -= carry9 << 21; - carry10 = s10 >> 21; - s11 += carry10; - s10 -= carry10 << 21; - - s[0] = (byte) s0; - s[1] = (byte) (s0 >> 8); - s[2] = (byte) ((s0 >> 16) | (s1 << 5)); - s[3] = (byte) (s1 >> 3); - s[4] = (byte) (s1 >> 11); - s[5] = (byte) ((s1 >> 19) | (s2 << 2)); - s[6] = (byte) (s2 >> 6); - s[7] = (byte) ((s2 >> 14) | (s3 << 7)); - s[8] = (byte) (s3 >> 1); - s[9] = (byte) (s3 >> 9); - s[10] = (byte) ((s3 >> 17) | (s4 << 4)); - s[11] = (byte) (s4 >> 4); - s[12] = (byte) (s4 >> 12); - s[13] = (byte) ((s4 >> 20) | (s5 << 1)); - s[14] = (byte) (s5 >> 7); - s[15] = (byte) ((s5 >> 15) | (s6 << 6)); - s[16] = (byte) (s6 >> 2); - s[17] = (byte) (s6 >> 10); - s[18] = (byte) ((s6 >> 18) | (s7 << 3)); - s[19] = (byte) (s7 >> 5); - s[20] = (byte) (s7 >> 13); - s[21] = (byte) s8; - s[22] = (byte) (s8 >> 8); - s[23] = (byte) ((s8 >> 16) | (s9 << 5)); - s[24] = (byte) (s9 >> 3); - s[25] = (byte) (s9 >> 11); - s[26] = (byte) ((s9 >> 19) | (s10 << 2)); - s[27] = (byte) (s10 >> 6); - s[28] = (byte) ((s10 >> 14) | (s11 << 7)); - s[29] = (byte) (s11 >> 1); - s[30] = (byte) (s11 >> 9); - s[31] = (byte) (s11 >> 17); - } - - // The order of the generator as unsigned bytes in little endian order. - // (2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed, cf. RFC 7748) - private static final byte[] GROUP_ORDER = { - (byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c, - (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58, - (byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2, - (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10}; - - // Checks whether s represents an integer smaller than the order of the group. - // This is needed to ensure that EdDSA signatures are non-malleable, as failing to check - // the range of S allows to modify signatures (cf. RFC 8032, Section 5.2.7 and Section 8.4.) - // @param s an integer in little-endian order. - private static boolean isSmallerThanGroupOrder(byte[] s) { - for (int j = Field25519.FIELD_LEN - 1; j >= 0; j--) { - // compare unsigned bytes - int a = s[j] & 0xff; - int b = GROUP_ORDER[j] & 0xff; - if (a != b) { - return a < b; - } - } - return false; - } - - /** - * Returns true if the EdDSA {@code signature} with {@code message}, can be verified with - * {@code publicKey}. - */ - public static boolean verify(final byte[] message, final byte[] signature, - final byte[] publicKey) { - try { - if (signature.length != SIGNATURE_LEN) { - return false; - } - if (publicKey.length != PUBLIC_KEY_LEN) { - return false; - } - byte[] s = Arrays.copyOfRange(signature, Field25519.FIELD_LEN, SIGNATURE_LEN); - if (!isSmallerThanGroupOrder(s)) { - return false; - } - MessageDigest digest = MessageDigest.getInstance("SHA-512"); - digest.update(signature, 0, Field25519.FIELD_LEN); - digest.update(publicKey); - digest.update(message); - byte[] h = digest.digest(); - reduce(h); - - XYZT negPublicKey = XYZT.fromBytesNegateVarTime(publicKey); - XYZ xyz = doubleScalarMultVarTime(h, negPublicKey, s); - byte[] expectedR = xyz.toBytes(); - for (int i = 0; i < Field25519.FIELD_LEN; i++) { - if (expectedR[i] != signature[i]) { - return false; - } - } - return true; - } catch (final GeneralSecurityException ignored) { - return false; - } - } -} diff --git a/app/src/main/java/com/wireguard/crypto/Key.java b/app/src/main/java/com/wireguard/crypto/Key.java deleted file mode 100644 index 9e25e6057..000000000 --- a/app/src/main/java/com/wireguard/crypto/Key.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.crypto; - -import com.wireguard.crypto.KeyFormatException.Type; - -import java.security.MessageDigest; -import java.security.SecureRandom; -import java.util.Arrays; - -/** - * Represents a WireGuard public or private key. This class uses specialized constant-time base64 - * and hexadecimal codec implementations that resist side-channel attacks. - *

- * Instances of this class are immutable. - */ -@SuppressWarnings("MagicNumber") -public final class Key { - private final byte[] key; - - /** - * Constructs an object encapsulating the supplied key. - * - * @param key an array of bytes containing a binary key. Callers of this constructor are - * responsible for ensuring that the array is of the correct length. - */ - private Key(final byte[] key) { - // Defensively copy to ensure immutability. - this.key = Arrays.copyOf(key, key.length); - } - - /** - * Decodes a single 4-character base64 chunk to an integer in constant time. - * - * @param src an array of at least 4 characters in base64 format - * @param srcOffset the offset of the beginning of the chunk in {@code src} - * @return the decoded 3-byte integer, or some arbitrary integer value if the input was not - * valid base64 - */ - private static int decodeBase64(final char[] src, final int srcOffset) { - int val = 0; - for (int i = 0; i < 4; ++i) { - final char c = src[i + srcOffset]; - val |= (-1 - + ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64)) - + ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70)) - + ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5)) - + ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63) - + ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64) - ) << (18 - 6 * i); - } - return val; - } - - /** - * Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time. - * - * @param src an array of at least 3 bytes - * @param srcOffset the offset of the beginning of the chunk in {@code src} - * @param dest an array of at least 4 characters - * @param destOffset the offset of the beginning of the chunk in {@code dest} - */ - private static void encodeBase64(final byte[] src, final int srcOffset, - final char[] dest, final int destOffset) { - final byte[] input = { - (byte) ((src[srcOffset] >>> 2) & 63), - (byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63), - (byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63), - (byte) ((src[2 + srcOffset]) & 63), - }; - for (int i = 0; i < 4; ++i) { - dest[i + destOffset] = (char) (input[i] + 'A' - + (((25 - input[i]) >>> 8) & 6) - - (((51 - input[i]) >>> 8) & 75) - - (((61 - input[i]) >>> 8) & 15) - + (((62 - input[i]) >>> 8) & 3)); - } - } - - /** - * Decodes a WireGuard public or private key from its base64 string representation. This - * function throws a {@link KeyFormatException} if the source string is not well-formed. - * - * @param str the base64 string representation of a WireGuard key - * @return the decoded key encapsulated in an immutable container - */ - public static Key fromBase64(final String str) throws KeyFormatException { - final char[] input = str.toCharArray(); - if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=') - throw new KeyFormatException(Format.BASE64, Type.LENGTH); - final byte[] key = new byte[Format.BINARY.length]; - int i; - int ret = 0; - for (i = 0; i < key.length / 3; ++i) { - final int val = decodeBase64(input, i * 4); - ret |= val >>> 31; - key[i * 3] = (byte) ((val >>> 16) & 0xff); - key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); - key[i * 3 + 2] = (byte) (val & 0xff); - } - final char[] endSegment = { - input[i * 4], - input[i * 4 + 1], - input[i * 4 + 2], - 'A', - }; - final int val = decodeBase64(endSegment, 0); - ret |= (val >>> 31) | (val & 0xff); - key[i * 3] = (byte) ((val >>> 16) & 0xff); - key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff); - - if (ret != 0) - throw new KeyFormatException(Format.BASE64, Type.CONTENTS); - return new Key(key); - } - - /** - * Wraps a WireGuard public or private key in an immutable container. This function throws a - * {@link KeyFormatException} if the source data is not the correct length. - * - * @param bytes an array of bytes containing a WireGuard key in binary format - * @return the key encapsulated in an immutable container - */ - public static Key fromBytes(final byte[] bytes) throws KeyFormatException { - if (bytes.length != Format.BINARY.length) - throw new KeyFormatException(Format.BINARY, Type.LENGTH); - return new Key(bytes); - } - - /** - * Decodes a WireGuard public or private key from its hexadecimal string representation. This - * function throws a {@link KeyFormatException} if the source string is not well-formed. - * - * @param str the hexadecimal string representation of a WireGuard key - * @return the decoded key encapsulated in an immutable container - */ - public static Key fromHex(final String str) throws KeyFormatException { - final char[] input = str.toCharArray(); - if (input.length != Format.HEX.length) - throw new KeyFormatException(Format.HEX, Type.LENGTH); - final byte[] key = new byte[Format.BINARY.length]; - int ret = 0; - for (int i = 0; i < key.length; ++i) { - int c; - int cNum; - int cNum0; - int cAlpha; - int cAlpha0; - int cVal; - final int cAcc; - - c = input[i * 2]; - cNum = c ^ 48; - cNum0 = ((cNum - 10) >>> 8) & 0xff; - cAlpha = (c & ~32) - 55; - cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff; - ret |= ((cNum0 | cAlpha0) - 1) >>> 8; - cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha); - cAcc = cVal * 16; - - c = input[i * 2 + 1]; - cNum = c ^ 48; - cNum0 = ((cNum - 10) >>> 8) & 0xff; - cAlpha = (c & ~32) - 55; - cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff; - ret |= ((cNum0 | cAlpha0) - 1) >>> 8; - cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha); - key[i] = (byte) (cAcc | cVal); - } - if (ret != 0) - throw new KeyFormatException(Format.HEX, Type.CONTENTS); - return new Key(key); - } - - /** - * Generates a private key using the system's {@link SecureRandom} number generator. - * - * @return a well-formed random private key - */ - static Key generatePrivateKey() { - final SecureRandom secureRandom = new SecureRandom(); - final byte[] privateKey = new byte[Format.BINARY.getLength()]; - secureRandom.nextBytes(privateKey); - privateKey[0] &= 248; - privateKey[31] &= 127; - privateKey[31] |= 64; - return new Key(privateKey); - } - - /** - * Generates a public key from an existing private key. - * - * @param privateKey a private key - * @return a well-formed public key that corresponds to the supplied private key - */ - static Key generatePublicKey(final Key privateKey) { - final byte[] publicKey = new byte[Format.BINARY.getLength()]; - Curve25519.eval(publicKey, 0, privateKey.getBytes(), null); - return new Key(publicKey); - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) - return true; - if (obj == null || obj.getClass() != getClass()) - return false; - final Key other = (Key) obj; - return MessageDigest.isEqual(key, other.key); - } - - /** - * Returns the key as an array of bytes. - * - * @return an array of bytes containing the raw binary key - */ - public byte[] getBytes() { - // Defensively copy to ensure immutability. - return Arrays.copyOf(key, key.length); - } - - @Override - public int hashCode() { - int ret = 0; - for (int i = 0; i < key.length / 4; ++i) - ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24); - return ret; - } - - /** - * Encodes the key to base64. - * - * @return a string containing the encoded key - */ - public String toBase64() { - final char[] output = new char[Format.BASE64.length]; - int i; - for (i = 0; i < key.length / 3; ++i) - encodeBase64(key, i * 3, output, i * 4); - final byte[] endSegment = { - key[i * 3], - key[i * 3 + 1], - 0, - }; - encodeBase64(endSegment, 0, output, i * 4); - output[Format.BASE64.length - 1] = '='; - return new String(output); - } - - /** - * Encodes the key to hexadecimal ASCII characters. - * - * @return a string containing the encoded key - */ - public String toHex() { - final char[] output = new char[Format.HEX.length]; - for (int i = 0; i < key.length; ++i) { - output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf) - + ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38)); - output[i * 2 + 1] = (char) (87 + (key[i] & 0xf) - + ((((key[i] & 0xf) - 10) >> 8) & ~38)); - } - return new String(output); - } - - /** - * The supported formats for encoding a WireGuard key. - */ - public enum Format { - BASE64(44), - BINARY(32), - HEX(64); - - private final int length; - - Format(final int length) { - this.length = length; - } - - public int getLength() { - return length; - } - } - -} diff --git a/app/src/main/java/com/wireguard/crypto/KeyFormatException.java b/app/src/main/java/com/wireguard/crypto/KeyFormatException.java deleted file mode 100644 index 5818b4d45..000000000 --- a/app/src/main/java/com/wireguard/crypto/KeyFormatException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.crypto; - -/** - * An exception thrown when attempting to parse an invalid key (too short, too long, or byte - * data inappropriate for the format). The format being parsed can be accessed with the - * {@link #getFormat} method. - */ -public final class KeyFormatException extends Exception { - private final Key.Format format; - private final Type type; - - KeyFormatException(final Key.Format format, final Type type) { - this.format = format; - this.type = type; - } - - public Key.Format getFormat() { - return format; - } - - public Type getType() { - return type; - } - - public enum Type { - CONTENTS, - LENGTH - } -} diff --git a/app/src/main/java/com/wireguard/crypto/KeyPair.java b/app/src/main/java/com/wireguard/crypto/KeyPair.java deleted file mode 100644 index f8238e91c..000000000 --- a/app/src/main/java/com/wireguard/crypto/KeyPair.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.wireguard.crypto; - -/** - * Represents a Curve25519 key pair as used by WireGuard. - *

- * Instances of this class are immutable. - */ -public class KeyPair { - private final Key privateKey; - private final Key publicKey; - - /** - * Creates a key pair using a newly-generated private key. - */ - public KeyPair() { - this(Key.generatePrivateKey()); - } - - /** - * Creates a key pair using an existing private key. - * - * @param privateKey a private key, used to derive the public key - */ - public KeyPair(final Key privateKey) { - this.privateKey = privateKey; - publicKey = Key.generatePublicKey(privateKey); - } - - /** - * Returns the private key from the key pair. - * - * @return the private key - */ - public Key getPrivateKey() { - return privateKey; - } - - /** - * Returns the public key from the key pair. - * - * @return the public key - */ - public Key getPublicKey() { - return publicKey; - } -} diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt index d44a3b601..caf363f6f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt @@ -16,6 +16,8 @@ object Key { const val MODE_VPN = "vpn" const val MODE_PROXY = "proxy" + const val GLOBAL_CUSTOM_CONFIG = "globalCustomConfig" + const val REMOTE_DNS = "remoteDns" const val DIRECT_DNS = "directDns" const val ENABLE_DNS_ROUTING = "enableDnsRouting" diff --git a/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt b/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt index 2cc881b40..f38305d46 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt @@ -20,6 +20,8 @@ import go.Seq import io.nekohasekai.sagernet.bg.SagerConnection import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.ktx.Logs +import io.nekohasekai.sagernet.ktx.isOss +import io.nekohasekai.sagernet.ktx.isPreview import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher import io.nekohasekai.sagernet.ui.MainActivity import io.nekohasekai.sagernet.utils.* @@ -27,6 +29,7 @@ import kotlinx.coroutines.DEBUG_PROPERTY_NAME import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON import libcore.Libcore import moe.matsuri.nb4a.NativeInterface +import moe.matsuri.nb4a.net.LocalResolverImpl import moe.matsuri.nb4a.utils.JavaUtil import moe.matsuri.nb4a.utils.cleanWebview import java.io.File @@ -51,10 +54,21 @@ class SagerNet : Application(), override fun onCreate() { super.onCreate() - System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON) Thread.setDefaultUncaughtExceptionHandler(CrashHandler) if (isMainProcess || isBgProcess) { + externalAssets.mkdirs() + Seq.setContext(this) + Libcore.initCore( + process, + cacheDir.absolutePath + "/", + filesDir.absolutePath + "/", + externalAssets.absolutePath + "/", + DataStore.logBufSize, + DataStore.logLevel > 0, + nativeInterface, nativeInterface, LocalResolverImpl + ) + // fix multi process issue in Android 9+ JavaUtil.handleWebviewDir(this) @@ -64,21 +78,6 @@ class SagerNet : Application(), } } - Seq.setContext(this) - updateNotificationChannels() - - // nb4a: init core - externalAssets.mkdirs() - Libcore.initCore( - process, - cacheDir.absolutePath + "/", - filesDir.absolutePath + "/", - externalAssets.absolutePath + "/", - DataStore.logBufSize, - DataStore.logLevel > 0, - nativeInterface, nativeInterface - ) - if (isMainProcess) { Theme.apply(this) Theme.applyNightTheme() @@ -86,17 +85,22 @@ class SagerNet : Application(), DefaultNetworkListener.start(this) { underlyingNetwork = it } + + updateNotificationChannels() } } - if (BuildConfig.DEBUG) StrictMode.setVmPolicy( - StrictMode.VmPolicy.Builder() - .detectLeakedSqlLiteObjects() - .detectLeakedClosableObjects() - .detectLeakedRegistrationObjects() - .penaltyLog() - .build() - ) + if (BuildConfig.DEBUG) { + System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON) + StrictMode.setVmPolicy( + StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .detectLeakedRegistrationObjects() + .penaltyLog() + .build() + ) + } } override fun onConfigurationChanged(newConfig: Configuration) { @@ -176,6 +180,10 @@ class SagerNet : Application(), "service-subscription", application.getText(R.string.service_subscription), NotificationManager.IMPORTANCE_DEFAULT + ), NotificationChannel( + "connection-test", + application.getText(R.string.connection_test), + NotificationManager.IMPORTANCE_DEFAULT ) ) ) @@ -194,6 +202,18 @@ class SagerNet : Application(), var underlyingNetwork: Network? = null + var appVersionNameForDisplay = { + var n = BuildConfig.VERSION_NAME + if (isPreview) { + n += " " + BuildConfig.PRE_VERSION_NAME + } else if (!isOss) { + n += " ${BuildConfig.FLAVOR}" + } + if (BuildConfig.DEBUG) { + n += " DEBUG" + } + n + }() } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt index 4d6950436..e760983dc 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt @@ -105,6 +105,10 @@ class BaseService { override fun getProfileName(): String = data?.proxy?.displayProfileName ?: "Idle" override fun registerCallback(cb: ISagerNetServiceCallback, id: Int) { + if (id == SagerConnection.CONNECTION_ID_RESTART_BG) { + Runtime.getRuntime().exit(0) + return + } if (!callbackIdMap.contains(cb)) { callbacks.register(cb) } diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt index f944058dc..5b860b35f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt @@ -3,24 +3,21 @@ package io.nekohasekai.sagernet.bg import android.system.ErrnoException import android.system.Os import android.system.OsConstants -import android.text.TextUtils import io.nekohasekai.sagernet.ktx.Logs import java.io.File import java.io.IOException +import androidx.core.text.isDigitsOnly object Executable { private val EXECUTABLES = setOf( - "libtrojan.so", - "libtrojan-go.so", - "libnaive.so", - "libtuic.so", - "libhysteria.so" + "libtrojan.so", "libtrojan-go.so", "libnaive.so", "libtuic.so", "libhysteria.so" ) fun killAll(alsoKillBg: Boolean = false) { - for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) } - ?: return) { - val exe = File(try { + // kill bg may fail + for (process in File("/proc").listFiles { _, name -> name.isDigitsOnly() } ?: return) { + val exe = File( + try { File(process, "cmdline").inputStream().bufferedReader().use { it.readText() } diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt index ab7d5a493..97ff4b8a0 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt @@ -32,6 +32,9 @@ class SagerConnection( const val CONNECTION_ID_TILE = 1 const val CONNECTION_ID_MAIN_ACTIVITY_FOREGROUND = 2 const val CONNECTION_ID_MAIN_ACTIVITY_BACKGROUND = 3 + const val CONNECTION_ID_RESTART_BG = 4 + + var restartingApp = false } interface Callback { @@ -124,7 +127,7 @@ class SagerConnection( } catch (e: RemoteException) { e.printStackTrace() } - callback!!.onServiceConnected(service) + callback?.onServiceConnected(service) } override fun onServiceDisconnected(name: ComponentName?) { @@ -137,7 +140,9 @@ class SagerConnection( override fun binderDied() { service = null callbackRegistered = false - callback?.also { runOnMainDispatcher { it.onBinderDied() } } + if (!restartingApp) { + callback?.also { runOnMainDispatcher { it.onBinderDied() } } + } } private fun unregisterCallback() { @@ -149,7 +154,7 @@ class SagerConnection( callbackRegistered = false } - fun connect(context: Context, callback: Callback) { + fun connect(context: Context, callback: Callback?) { if (connectionActive) return connectionActive = true check(this.callback == null) diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt index 84586bc2c..9f16723de 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt @@ -21,6 +21,7 @@ import io.nekohasekai.sagernet.plugin.PluginManager import kotlinx.coroutines.* import libcore.BoxInstance import libcore.Libcore +import moe.matsuri.nb4a.net.LocalResolverImpl import java.io.File abstract class BoxInstance( @@ -48,7 +49,7 @@ abstract class BoxInstance( } protected open suspend fun loadConfig() { - box = Libcore.newSingBoxInstance(config.config) + box = Libcore.newSingBoxInstance(config.config, LocalResolverImpl) } open suspend fun init() { diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt index 293c65742..9758a5c40 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt @@ -7,8 +7,6 @@ import io.nekohasekai.sagernet.database.ProxyEntity import io.nekohasekai.sagernet.ktx.Logs import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher import kotlinx.coroutines.runBlocking -import libcore.Libcore -import moe.matsuri.nb4a.net.LocalResolverImpl import moe.matsuri.nb4a.utils.JavaUtil class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? = null) : @@ -45,7 +43,6 @@ class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? = } override suspend fun loadConfig() { - Libcore.registerLocalDNSTransport(LocalResolverImpl) super.loadConfig() } @@ -59,7 +56,6 @@ class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? = } override fun close() { - Libcore.registerLocalDNSTransport(null) super.close() runBlocking { looper?.stop() diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt index 57d1a1702..bc6907137 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt @@ -10,6 +10,7 @@ import io.nekohasekai.sagernet.ktx.tryResume import io.nekohasekai.sagernet.ktx.tryResumeWithException import kotlinx.coroutines.delay import libcore.Libcore +import moe.matsuri.nb4a.net.LocalResolverImpl import kotlin.coroutines.suspendCoroutine class TestInstance(profile: ProxyEntity, val link: String, private val timeout: Int) : @@ -46,7 +47,7 @@ class TestInstance(profile: ProxyEntity, val link: String, private val timeout: override suspend fun loadConfig() { // don't call destroyAllJsi here if (BuildConfig.DEBUG) Logs.d(config.config) - box = Libcore.newSingBoxInstance(config.config) + box = Libcore.newSingBoxInstance(config.config, LocalResolverImpl) } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt index df6f06ab4..06bb50133 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt @@ -17,7 +17,6 @@ import io.nekohasekai.sagernet.ktx.int import io.nekohasekai.sagernet.ktx.long import io.nekohasekai.sagernet.ktx.parsePort import io.nekohasekai.sagernet.ktx.string -import io.nekohasekai.sagernet.ktx.stringSet import io.nekohasekai.sagernet.ktx.stringToInt import io.nekohasekai.sagernet.ktx.stringToIntIfExists import moe.matsuri.nb4a.TempDatabase @@ -41,6 +40,10 @@ object DataStore : OnPreferenceDataStoreChangeListener { var vpnService: VpnService? = null var baseService: BaseService.Interface? = null + // main + + var runningTest = false + fun currentGroupId(): Long { val currentSelected = configurationStore.getLong(Key.PROFILE_GROUP, -1) if (currentSelected > 0L) return currentSelected @@ -108,10 +111,12 @@ object DataStore : OnPreferenceDataStoreChangeListener { var speedInterval by configurationStore.stringToInt(Key.SPEED_INTERVAL) var showGroupInNotification by configurationStore.boolean("showGroupInNotification") + var globalCustomConfig by configurationStore.string(Key.GLOBAL_CUSTOM_CONFIG) { "" } + var remoteDns by configurationStore.string(Key.REMOTE_DNS) { "https://dns.google/dns-query" } var directDns by configurationStore.string(Key.DIRECT_DNS) { "https://223.5.5.5/dns-query" } var enableDnsRouting by configurationStore.boolean(Key.ENABLE_DNS_ROUTING) { true } - var enableFakeDns by configurationStore.boolean(Key.ENABLE_FAKEDNS) + var enableFakeDns by configurationStore.boolean(Key.ENABLE_FAKEDNS) { true } var rulesProvider by configurationStore.stringToInt(Key.RULES_PROVIDER) var logLevel by configurationStore.stringToInt(Key.LOG_LEVEL) @@ -154,7 +159,7 @@ object DataStore : OnPreferenceDataStoreChangeListener { var connectionTestConcurrent by configurationStore.int("connectionTestConcurrent") { 5 } var alwaysShowAddress by configurationStore.boolean(Key.ALWAYS_SHOW_ADDRESS) - var tunImplementation by configurationStore.stringToInt(Key.TUN_IMPLEMENTATION) { TunImplementation.MIXED } + var tunImplementation by configurationStore.stringToInt(Key.TUN_IMPLEMENTATION) { TunImplementation.GVISOR } var profileTrafficStatistics by configurationStore.boolean(Key.PROFILE_TRAFFIC_STATISTICS) { true } var yacdURL by configurationStore.string("yacdURL") { "http://127.0.0.1:9090/ui" } diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt b/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt index 8049cf89a..ba21dde44 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt @@ -200,18 +200,6 @@ object ProfileManager { outbound = -2 ) ) - createRule( - RuleEntity( - name = app.getString(R.string.route_opt_block_analysis), - domains = app.assets.open("analysis.txt").use { - it.bufferedReader() - .readLines() - .filter { it.isNotBlank() } - .joinToString("\n") - }, - outbound = -2, - ) - ) val fuckedCountry = mutableListOf("cn:中国") if (Locale.getDefault().country != Locale.CHINA.country) { // 非中文用户 diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt b/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt index 7fb409dc4..7d610c5e4 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt @@ -12,6 +12,8 @@ import kotlinx.parcelize.Parcelize data class RuleEntity( @PrimaryKey(autoGenerate = true) var id: Long = 0L, var name: String = "", + @ColumnInfo(defaultValue = "") + var config: String = "", var userOrder: Long = 0L, var enabled: Boolean = false, var domains: String = "", @@ -31,11 +33,12 @@ data class RuleEntity( fun mkSummary(): String { var summary = "" + if (config.isNotBlank()) summary += "[config]\n" if (domains.isNotBlank()) summary += "$domains\n" if (ip.isNotBlank()) summary += "$ip\n" - if (source.isNotBlank()) summary += "source: $source\n" - if (sourcePort.isNotBlank()) summary += "sourcePort: $sourcePort\n" - if (port.isNotBlank()) summary += "port: $port\n" + if (source.isNotBlank()) summary += "src ip: $source\n" + if (sourcePort.isNotBlank()) summary += "src port: $sourcePort\n" + if (port.isNotBlank()) summary += "dst port: $port\n" if (network.isNotBlank()) summary += "network: $network\n" if (protocol.isNotBlank()) summary += "protocol: $protocol\n" if (packages.isNotEmpty()) summary += app.getString( diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt index cb9cade6b..ececc106e 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt @@ -16,10 +16,11 @@ import kotlinx.coroutines.launch @Database( entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class], - version = 5, + version = 6, autoMigrations = [ AutoMigration(from = 3, to = 4), - AutoMigration(from = 4, to = 5) + AutoMigration(from = 4, to = 5), + AutoMigration(from = 5, to = 6) ] ) @TypeConverters(value = [KryoConverters::class, GsonConverters::class]) @@ -33,6 +34,7 @@ abstract class SagerDatabase : RoomDatabase() { SagerNet.application.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs() Room.databaseBuilder(SagerNet.application, SagerDatabase::class.java, Key.DB_PROFILE) // .addMigrations(*SagerDatabase_Migrations.build()) + .setJournalMode(JournalMode.TRUNCATE) .allowMainThreadQueries() .enableMultiInstanceInvalidation() .fallbackToDestructiveMigration() diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt b/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt index e9296de55..d4ebf9800 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt @@ -16,6 +16,7 @@ abstract class PublicDatabase : RoomDatabase() { val instance by lazy { SagerNet.application.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs() Room.databaseBuilder(SagerNet.application, PublicDatabase::class.java, Key.DB_PUBLIC) + .setJournalMode(JournalMode.TRUNCATE) .allowMainThreadQueries() .enableMultiInstanceInvalidation() .fallbackToDestructiveMigration() diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java index 53dda06c2..86e6fca9e 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java @@ -144,8 +144,4 @@ public int hashCode() { public String toString() { return getClass().getSimpleName() + " " + JavaUtil.gson.toJson(this); } - - public void applyFeatureSettings(AbstractBean other) { - } - } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt index 64bd0b905..8fe0a2944 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt @@ -82,7 +82,6 @@ fun buildConfig( val globalOutbounds = HashMap() val selectorNames = ArrayList() val group = SagerDatabase.groupDao.getById(proxy.groupId) - val optionsToMerge = proxy.requireBean().customConfigJson ?: "" fun ProxyEntity.resolveChainInternal(): MutableList { val bean = requireBean() @@ -162,12 +161,6 @@ fun buildConfig( external_controller = "127.0.0.1:9090" external_ui = "../files/yacd" } - - cache_file = CacheFile().apply { - enabled = true - store_fakeip = true - path = "../cache/clash.db" - } } log = LogOptions().apply { @@ -261,13 +254,13 @@ fun buildConfig( add(entity) } - var currentOutbound = mutableMapOf() - lateinit var pastOutbound: MutableMap + var currentOutbound: SingBoxOption + lateinit var pastOutbound: SingBoxOption lateinit var pastInboundTag: String var pastEntity: ProxyEntity? = null val externalChainMap = LinkedHashMap() externalIndexMap.add(IndexEntity(externalChainMap)) - val chainOutbounds = ArrayList>() + val chainOutbounds = ArrayList() // chainTagOut: v2ray outbound tag for this chain var chainTagOut = "" @@ -315,7 +308,7 @@ fun buildConfig( outbound = tagOut }) } else { - pastOutbound["detour"] = tagOut + pastOutbound._hack_config_map["detour"] = tagOut } } else { // index == 0 means last profile in chain / not chain @@ -338,53 +331,49 @@ fun buildConfig( type = "socks" server = LOCALHOST server_port = localPort - }.asMap() - } else { // internal outbound + } + } else { + // internal outbound + currentOutbound = when (bean) { - is ConfigBean -> - gson.fromJson(bean.config, currentOutbound.javaClass) + is ConfigBean -> CustomSingBoxOption(bean.config) is ShadowTLSBean -> // before StandardV2RayBean - buildSingBoxOutboundShadowTLSBean(bean).asMap() + buildSingBoxOutboundShadowTLSBean(bean) is StandardV2RayBean -> // http/trojan/vmess/vless - buildSingBoxOutboundStandardV2RayBean(bean).asMap() + buildSingBoxOutboundStandardV2RayBean(bean) is HysteriaBean -> buildSingBoxOutboundHysteriaBean(bean) is TuicBean -> - buildSingBoxOutboundTuicBean(bean).asMap() + buildSingBoxOutboundTuicBean(bean) is SOCKSBean -> - buildSingBoxOutboundSocksBean(bean).asMap() + buildSingBoxOutboundSocksBean(bean) is ShadowsocksBean -> - buildSingBoxOutboundShadowsocksBean(bean).asMap() + buildSingBoxOutboundShadowsocksBean(bean) is WireGuardBean -> - buildSingBoxOutboundWireguardBean(bean).asMap() + buildSingBoxOutboundWireguardBean(bean) is SSHBean -> - buildSingBoxOutboundSSHBean(bean).asMap() + buildSingBoxOutboundSSHBean(bean) is AnyTLSBean -> - buildSingBoxOutboundAnyTLSBean(bean).asMap() + buildSingBoxOutboundAnyTLSBean(bean) else -> throw IllegalStateException("can't reach") } - currentOutbound.apply { - // TODO nb4a keepAliveInterval? -// val keepAliveInterval = DataStore.tcpKeepAliveInterval -// val needKeepAliveInterval = keepAliveInterval !in intArrayOf(0, 15) - - if (!muxApplied) { - val muxObj = proxyEntity.singMux() - if (muxObj != null && muxObj.enabled) { - muxApplied = true - currentOutbound["multiplex"] = muxObj.asMap() - } + // internal mux + if (!muxApplied) { + val muxObj = proxyEntity.singMux() + if (muxObj != null && muxObj.enabled) { + muxApplied = true + currentOutbound._hack_config_map["multiplex"] = muxObj.asMap() } } } @@ -394,8 +383,8 @@ fun buildConfig( // udp over tcp try { val sUoT = bean.javaClass.getField("sUoT").get(bean) - if (sUoT is Boolean && sUoT == true) { - currentOutbound["udp_over_tcp"] = true + if (sUoT is Boolean && sUoT) { + _hack_config_map["udp_over_tcp"] = true } } catch (_: Exception) { } @@ -407,19 +396,13 @@ fun buildConfig( domainListDNSDirectForce.add("full:$serverAddress") } } - currentOutbound["domain_strategy"] = + _hack_config_map["domain_strategy"] = if (forTest) "" else defaultServerDomainStrategy - // custom JSON merge - if (bean.customOutboundJson.isNotBlank()) { - Util.mergeJSON( - bean.customOutboundJson, - currentOutbound as MutableMap - ) - } - } + _hack_config_map["tag"] = tagOut - currentOutbound["tag"] = tagOut + _hack_custom_config = bean.customOutboundJson + } // External proxy need a dokodemo-door inbound to forward the traffic // For external proxy software, their traffic must goes to v2ray-core to use protected fd. @@ -478,8 +461,8 @@ fun buildConfig( // build outbounds if (buildSelector) { - val list = group?.id?.let { SagerDatabase.proxyDao.getByGroup(it) } - list?.forEach { + val list = group.id.let { SagerDatabase.proxyDao.getByGroup(it) } + list.forEach { tagMap[it.id] = buildChain(it.id, it) } outbounds.add(0, Outbound_SelectorOptions().apply { @@ -487,7 +470,7 @@ fun buildConfig( tag = TAG_PROXY default_ = tagMap[proxy.id] outbounds = tagMap.values.toList() - }.asMap()) + }) } else { buildChain(0, proxy) } @@ -597,6 +580,8 @@ fun buildConfig( -2L -> TAG_BLOCK else -> if (outId == proxy.id) TAG_PROXY else tagMap[outId] ?: "" } + + _hack_custom_config = rule.config } if (!ruleObj.checkEmpty()) { @@ -626,7 +611,7 @@ fun buildConfig( for (freedom in arrayOf(TAG_DIRECT, TAG_BYPASS)) outbounds.add(Outbound().apply { tag = freedom type = "direct" - }.asMap()) + }) // Bypass Lookup for the first profile bypassDNSBeans.forEach { @@ -657,18 +642,17 @@ fun buildConfig( } } - // remote dns obj - remoteDns.firstOrNull().let { - // Always use direct DNS for urlTest - if (!forTest) dns.servers.add(DNSServerOptions().apply { - address = it ?: throw Exception("No remote DNS, check your settings!") - tag = "dns-remote" - address_resolver = "dns-direct" - strategy = autoDnsDomainStrategy(SingBoxOptionsUtil.domainStrategy(tag)) - }) - } + dns.servers.add(DNSServerOptions().apply { + address = "rcode://success" + tag = "dns-block" + }) + + dns.servers.add(DNSServerOptions().apply { + address = "local" + tag = "dns-local" + detour = TAG_DIRECT + }) - // add directDNS objects here directDNS.firstOrNull().let { dns.servers.add(DNSServerOptions().apply { address = it ?: throw Exception("No direct DNS, check your settings!") @@ -678,15 +662,18 @@ fun buildConfig( strategy = autoDnsDomainStrategy(SingBoxOptionsUtil.domainStrategy(tag)) }) } - dns.servers.add(DNSServerOptions().apply { - address = "local" - tag = "dns-local" - detour = TAG_DIRECT - }) - dns.servers.add(DNSServerOptions().apply { - address = "rcode://success" - tag = "dns-block" - }) + + remoteDns.firstOrNull().let { + // Always use direct DNS for urlTest + if (!forTest) dns.servers.add(DNSServerOptions().apply { + address = it ?: throw Exception("No remote DNS, check your settings!") + tag = "dns-remote" + address_resolver = "dns-direct" + strategy = autoDnsDomainStrategy(SingBoxOptionsUtil.domainStrategy(tag)) + }) + } + + dns.final_ = if (forTest) "dns-direct" else "dns-remote" // dns object user rules if (enableDnsRouting) { @@ -750,16 +737,18 @@ fun buildConfig( }) } } + + if (!forTest) _hack_custom_config = DataStore.globalCustomConfig }.let { + val configMap = it.asMap() + Util.mergeJSON(configMap, proxy.requireBean().customConfigJson) ConfigBuildResult( - gson.toJson(it.asMap().apply { - Util.mergeJSON(optionsToMerge, this) - }), + gson.toJson(configMap), externalIndexMap, proxy.id, trafficMap, tagMap, - if (buildSelector) group!!.id else -1L + if (buildSelector) group.id else -1L ) } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java index 6a5f83c03..2d2e19e1d 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java @@ -150,17 +150,6 @@ public void deserialize(ByteBufferInput input) { } } - @Override - public void applyFeatureSettings(AbstractBean other) { - if (!(other instanceof HysteriaBean)) return; - HysteriaBean bean = ((HysteriaBean) other); - bean.uploadMbps = uploadMbps; - bean.downloadMbps = downloadMbps; - bean.allowInsecure = allowInsecure; - bean.disableMtuDiscovery = disableMtuDiscovery; - bean.hopInterval = hopInterval; - } - @Override public String displayAddress() { return NetsKt.wrapIPV6Host(serverAddress) + ":" + serverPorts; diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt index 22dfeac52..1e78a29f5 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt @@ -272,7 +272,7 @@ fun HysteriaBean.canUseSingBox(): Boolean { return true } -fun buildSingBoxOutboundHysteriaBean(bean: HysteriaBean): MutableMap { +fun buildSingBoxOutboundHysteriaBean(bean: HysteriaBean): SingBoxOptions.SingBoxOption { return when (bean.protocolVersion) { 1 -> SingBoxOptions.Outbound_HysteriaOptions().apply { type = "hysteria" @@ -311,7 +311,7 @@ fun buildSingBoxOutboundHysteriaBean(bean: HysteriaBean): MutableMap SingBoxOptions.Outbound_Hysteria2Options().apply { type = "hysteria2" @@ -350,9 +350,9 @@ fun buildSingBoxOutboundHysteriaBean(bean: HysteriaBean): MutableMap mutableMapOf("error_version" to bean.protocolVersion) + else -> error("error_version $bean.protocolVersion") } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java index 326662ffb..10c394f59 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java @@ -50,13 +50,6 @@ public void deserialize(ByteBufferInput input) { sUoT = input.readBoolean(); } - @Override - public void applyFeatureSettings(AbstractBean other) { - if (!(other instanceof ShadowsocksBean)) return; - ShadowsocksBean bean = ((ShadowsocksBean) other); - bean.sUoT = sUoT; - } - @NotNull @Override public ShadowsocksBean clone() { diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt index 04506abdc..b1d0bb9c1 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt @@ -117,6 +117,10 @@ fun buildSingBoxOutboundShadowsocksBean(bean: ShadowsocksBean): SingBoxOptions.O if (bean.plugin.isNotBlank()) { plugin = bean.plugin.substringBefore(";") plugin_opts = bean.plugin.substringAfter(";") + if (plugin == "none") { + plugin = null + plugin_opts = null + } } } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoBean.java index f02f36388..fa4aaf4e5 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoBean.java @@ -146,15 +146,6 @@ public void deserialize(ByteBufferInput input) { } } - @Override - public void applyFeatureSettings(AbstractBean other) { - if (!(other instanceof TrojanGoBean)) return; - TrojanGoBean bean = ((TrojanGoBean) other); - if (allowInsecure) { - bean.allowInsecure = true; - } - } - @NotNull @Override public TrojanGoBean clone() { diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt index cad2147e6..b418a1b63 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt @@ -17,11 +17,21 @@ fun parseTuic(url: String): TuicBean { protocolVersion = 5 name = link.fragment - uuid = link.username - token = link.password serverAddress = link.host serverPort = link.port + val rawUser = link.username + val rawPass = link.password + + if (rawUser.contains(":")) { + val parts = rawUser.split(":", limit = 2) + uuid = parts[0] + token = parts.getOrElse(1) { "" } + } else { + uuid = rawUser + token = rawPass + } + link.queryParameter("sni")?.let { sni = it } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java index 61a2a7323..22adb2539 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java @@ -112,7 +112,7 @@ public void initializeDefaultValues() { @Override public void serialize(ByteBufferOutput output) { - output.writeInt(3); + output.writeInt(4); super.serialize(output); output.writeString(uuid); output.writeString(encryption); @@ -133,18 +133,15 @@ public void serialize(ByteBufferOutput output) { output.writeString(earlyDataHeaderName); break; } - case "http": { + case "http": + case "httpupgrade": { output.writeString(host); output.writeString(path); break; } case "grpc": { output.writeString(path); - } - case "httpupgrade": { - output.writeString(host); - output.writeString(path); - + break; } } @@ -193,17 +190,20 @@ public void deserialize(ByteBufferInput input) { earlyDataHeaderName = input.readString(); break; } - case "http": { + case "http": + case "httpupgrade": { host = input.readString(); path = input.readString(); break; } case "grpc": { path = input.readString(); - } - case "httpupgrade": { - host = input.readString(); - path = input.readString(); + if (version < 4) { + // 解决老版本数据的读取问题 + input.readString(); + input.readString(); + } + break; } } @@ -258,21 +258,6 @@ public void deserialize(ByteBufferInput input) { } } - @Override - public void applyFeatureSettings(AbstractBean other) { - if (!(other instanceof StandardV2RayBean)) return; - StandardV2RayBean bean = ((StandardV2RayBean) other); - bean.allowInsecure = allowInsecure; - bean.utlsFingerprint = utlsFingerprint; - bean.packetEncoding = packetEncoding; - bean.enableECH = enableECH; - bean.echConfig = echConfig; - bean.enableMux = enableMux; - bean.muxPadding = muxPadding; - bean.muxType = muxType; - bean.muxConcurrency = muxConcurrency; - } - public boolean isVLESS() { if (this instanceof VMessBean) { Integer aid = ((VMessBean) this).alterId; diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt index 943674bd8..a7597999e 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt @@ -58,6 +58,7 @@ object RawUpdater : GroupUpdater() { val response = Libcore.newHttpClient().apply { trySocks5(DataStore.mixedPort) + tryH3Direct() when (DataStore.appTLSVersion) { "1.3" -> restrictedTLS() } @@ -73,6 +74,17 @@ object RawUpdater : GroupUpdater() { subscription.subscriptionUserinfo = Util.getStringBox(response.getHeader("Subscription-Userinfo")) + + // 修改默认名字 + if (proxyGroup.name?.startsWith("Subscription #") == true) { + var remoteName = Util.getStringBox(response.getHeader("content-disposition")) + if (remoteName.isNotBlank()) { + remoteName = Util.decodeFilename(remoteName) + if (remoteName.isNotBlank()) { + proxyGroup.name = remoteName + } + } + } } val proxiesMap = LinkedHashMap() @@ -149,7 +161,9 @@ object RawUpdater : GroupUpdater() { if (toReplace.contains(name)) { val entity = toReplace[name]!! val existsBean = entity.requireBean() - existsBean.applyFeatureSettings(bean) + // 更新订阅,保留自定义覆写设置 + bean.customOutboundJson = existsBean.customOutboundJson + bean.customConfigJson = existsBean.customConfigJson when { existsBean != bean -> { changed++ @@ -463,6 +477,15 @@ object RawUpdater : GroupUpdater() { } } } + + "ech-opts" -> (opt.value as? Map)?.also { + for (echOpt in it) { + when (echOpt.key) { + "enable" -> bean.enableECH = + echOpt.value.toString() == "true" + } + } + } } } proxies.add(bean) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt index 37747191e..6c0f8f45b 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt @@ -17,6 +17,7 @@ import io.nekohasekai.sagernet.fmt.v2ray.parseV2Ray import moe.matsuri.nb4a.proxy.anytls.parseAnytls import moe.matsuri.nb4a.utils.JavaUtil.gson import moe.matsuri.nb4a.utils.Util +import okhttp3.HttpUrl import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -137,6 +138,14 @@ suspend fun parseProxies(text: String): List { entities.add(parseHttp(this)) }.onFailure { Logs.w(it) + val clashUrl = HttpUrl.Builder() + .scheme("https") + .host("install-config") + .addQueryParameter("url", this) + .build() + .toString() + .replaceFirst("https://", "clash://") + throw (SubscriptionFoundException(clashUrl)) } } else if (startsWith("vmess://")) { Logs.d("Try parse v2ray link: $this") diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt index 98bbc20cf..3e176c8ed 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt @@ -20,6 +20,8 @@ import androidx.activity.result.ActivityResultLauncher import androidx.annotation.AttrRes import androidx.annotation.ColorRes import androidx.core.content.ContextCompat +import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -30,10 +32,10 @@ import com.jakewharton.processphoenix.ProcessPhoenix import io.nekohasekai.sagernet.BuildConfig import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.SagerNet -import io.nekohasekai.sagernet.bg.Executable +import io.nekohasekai.sagernet.aidl.ISagerNetService +import io.nekohasekai.sagernet.bg.BaseService +import io.nekohasekai.sagernet.bg.SagerConnection import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.database.SagerDatabase -import io.nekohasekai.sagernet.database.preference.PublicDatabase import io.nekohasekai.sagernet.ui.MainActivity import io.nekohasekai.sagernet.ui.ThemedActivity import kotlinx.coroutines.Dispatchers @@ -201,7 +203,7 @@ val shortAnimTime by lazy { fun View.crossFadeFrom(other: View) { clearAnimation() other.clearAnimation() - if (visibility == View.VISIBLE && other.visibility == View.GONE) return + if (isVisible && other.isGone) return alpha = 0F visibility = View.VISIBLE animate().alpha(1F).duration = shortAnimTime @@ -248,16 +250,33 @@ fun Fragment.needReload() { fun Fragment.needRestart() { snackbar(R.string.need_restart).setAction(R.string.apply) { + triggerFullRestart(requireContext()) + }.show() +} + +fun triggerFullRestart(ctx: Context) { + runOnDefaultDispatcher { SagerNet.stopService() - val ctx = requireContext() - runOnDefaultDispatcher { - delay(500) - SagerDatabase.instance.close() - PublicDatabase.instance.close() - Executable.killAll(true) + delay(500) + SagerConnection.restartingApp = true + val connection = SagerConnection(SagerConnection.CONNECTION_ID_RESTART_BG) + connection.connect(ctx, RestartCallback { ProcessPhoenix.triggerRebirth(ctx, Intent(ctx, MainActivity::class.java)) - } - }.show() + }) + } +} + +private class RestartCallback(val callback: () -> Unit) : SagerConnection.Callback { + override fun stateChanged( + state: BaseService.State, + profileName: String?, + msg: String? + ) { + } + + override fun onServiceConnected(service: ISagerNetService) { + callback() + } } fun Context.getColour(@ColorRes colorRes: Int): Int { @@ -271,11 +290,9 @@ fun Context.getColorAttr(@AttrRes resId: Int): Int { } val isExpert: Boolean by lazy { BuildConfig.DEBUG || DataStore.isExpert } - -val isExpertFlavor = ((BuildConfig.FLAVOR == "expert") || BuildConfig.DEBUG) const val isOss = BuildConfig.FLAVOR == "oss" -const val isFdroid = BuildConfig.FLAVOR == "fdroid" const val isPlay = BuildConfig.FLAVOR == "play" +const val isPreview = BuildConfig.FLAVOR == "preview" fun Continuation.tryResume(value: T) { try { diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt index 54ed72a2e..f6afc4b3f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt @@ -10,6 +10,7 @@ import android.os.PowerManager import android.provider.Settings import android.text.util.Linkify import android.view.View +import android.widget.Toast import androidx.activity.result.component1 import androidx.activity.result.component2 import androidx.activity.result.contract.ActivityResultContracts @@ -28,6 +29,12 @@ import io.nekohasekai.sagernet.utils.PackageCache import io.nekohasekai.sagernet.widget.ListListener import libcore.Libcore import moe.matsuri.nb4a.plugin.Plugins +import androidx.core.net.toUri +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import io.nekohasekai.sagernet.SagerNet +import io.nekohasekai.sagernet.database.DataStore +import moe.matsuri.nb4a.utils.Util +import org.json.JSONObject class AboutFragment : ToolbarFragment(R.layout.layout_about) { @@ -65,128 +72,133 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) { } override fun getMaterialAboutList(activityContext: Context): MaterialAboutList { - - var versionName = BuildConfig.VERSION_NAME - if (!isOss) { - versionName += " ${BuildConfig.FLAVOR}" - } - if (BuildConfig.DEBUG) { - versionName += " DEBUG" - } - return MaterialAboutList.Builder() .addCard( MaterialAboutCard.Builder() - .outline(false) - .addItem( - MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_update_24) - .text(R.string.app_version) - .subText(versionName) - .setOnClickAction { - requireContext().launchCustomTab( - "https://github.com/MatsuriDayo/NekoBoxForAndroid/releases" - ) - } - .build()) - .addItem( - MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_layers_24) - .text(getString(R.string.version_x, "sing-box")) - .subText(Libcore.versionBox()) - .setOnClickAction { } - .build()) - .addItem( - MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_card_giftcard_24) - .text(R.string.donate) - .subText(R.string.donate_info) - .setOnClickAction { - requireContext().launchCustomTab( - "https://matsuridayo.github.io/index_docs/#donate" - ) - } - .build()) - .apply { - PackageCache.awaitLoadSync() - for ((_, pkg) in PackageCache.installedPluginPackages) { - try { - val pluginId = - pkg.providers?.get(0)?.loadString(Plugins.METADATA_KEY_ID) - if (pluginId.isNullOrBlank()) continue - addItem( - MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_nfc_24) - .text( - getString( - R.string.version_x, - pluginId - ) + " (${Plugins.displayExeProvider(pkg.packageName)})" + .outline(false) + .addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_update_24) + .text(R.string.app_version) + .subText(SagerNet.appVersionNameForDisplay) + .setOnClickAction { + requireContext().launchCustomTab( + "https://github.com/MatsuriDayo/NekoBoxForAndroid/releases" + ) + } + .build()) + .addItem( + MaterialAboutActionItem.Builder() + .text(R.string.check_update_release) + .setOnClickAction { + checkUpdate(false) + } + .build()) + .addItem( + MaterialAboutActionItem.Builder() + .text(R.string.check_update_preview) + .setOnClickAction { + checkUpdate(true) + } + .build()) + .addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_layers_24) + .text(getString(R.string.version_x, "sing-box")) + .subText(Libcore.versionBox()) + .setOnClickAction { } + .build()) + .addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_card_giftcard_24) + .text(R.string.donate) + .subText(R.string.donate_info) + .setOnClickAction { + requireContext().launchCustomTab( + "https://matsuridayo.github.io/index_docs/#donate" ) - .subText("v" + pkg.versionName) - .setOnClickAction { - startActivity(Intent().apply { - action = - Settings.ACTION_APPLICATION_DETAILS_SETTINGS - data = Uri.fromParts( - "package", pkg.packageName, null + } + .build()) + .apply { + PackageCache.awaitLoadSync() + for ((_, pkg) in PackageCache.installedPluginPackages) { + try { + val pluginId = + pkg.providers?.get(0)?.loadString(Plugins.METADATA_KEY_ID) + if (pluginId.isNullOrBlank()) continue + addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_nfc_24) + .text( + getString( + R.string.version_x, + pluginId + ) + " (${Plugins.displayExeProvider(pkg.packageName)})" ) - }) - } - .build()) - } catch (e: Exception) { - Logs.w(e) + .subText("v" + pkg.versionName) + .setOnClickAction { + startActivity(Intent().apply { + action = + Settings.ACTION_APPLICATION_DETAILS_SETTINGS + data = Uri.fromParts( + "package", pkg.packageName, null + ) + }) + } + .build()) + } catch (e: Exception) { + Logs.w(e) + } } } - } - .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val pm = app.getSystemService(Context.POWER_SERVICE) as PowerManager - if (!pm.isIgnoringBatteryOptimizations(app.packageName)) { - addItem( - MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_running_with_errors_24) - .text(R.string.ignore_battery_optimizations) - .subText(R.string.ignore_battery_optimizations_sum) - .setOnClickAction { - requestIgnoreBatteryOptimizations.launch( - Intent( - Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, - Uri.parse("package:${app.packageName}") - ) - ) - } - .build()) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val pm = app.getSystemService(Context.POWER_SERVICE) as PowerManager + if (!pm.isIgnoringBatteryOptimizations(app.packageName)) { + addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_running_with_errors_24) + .text(R.string.ignore_battery_optimizations) + .subText(R.string.ignore_battery_optimizations_sum) + .setOnClickAction { + requestIgnoreBatteryOptimizations.launch( + Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + "package:${app.packageName}".toUri() + ) + ) + } + .build()) + } } } - } - .build()) + .build()) .addCard( MaterialAboutCard.Builder() - .outline(false) - .title(R.string.project) - .addItem( - MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_baseline_sanitizer_24) - .text(R.string.github) - .setOnClickAction { - requireContext().launchCustomTab( - "https://github.com/MatsuriDayo/NekoBoxForAndroid" - - ) - } - .build()) - .addItem( - MaterialAboutActionItem.Builder() - .icon(R.drawable.ic_qu_shadowsocks_foreground) - .text(R.string.telegram) - .setOnClickAction { - requireContext().launchCustomTab( - "https://t.me/MatsuriDayo" - ) - } + .outline(false) + .title(R.string.project) + .addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_baseline_sanitizer_24) + .text(R.string.github) + .setOnClickAction { + requireContext().launchCustomTab( + "https://github.com/MatsuriDayo/NekoBoxForAndroid" + + ) + } + .build()) + .addItem( + MaterialAboutActionItem.Builder() + .icon(R.drawable.ic_qu_shadowsocks_foreground) + .text(R.string.telegram) + .setOnClickAction { + requireContext().launchCustomTab( + "https://t.me/MatsuriDayo" + ) + } + .build()) .build()) - .build()) .build() } @@ -199,6 +211,69 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) { } } + fun checkUpdate(checkPreview: Boolean) { + runOnIoDispatcher { + try { + val client = Libcore.newHttpClient().apply { + modernTLS() + trySocks5(DataStore.mixedPort) + } + val response = client.newRequest().apply { + if (checkPreview) { + setURL("https://api.github.com/repos/MatsuriDayo/NekoBoxForAndroid/releases/tags/preview") + } else { + setURL("https://api.github.com/repos/MatsuriDayo/NekoBoxForAndroid/releases/latest") + } + }.execute() + val release = JSONObject(Util.getStringBox(response.contentString)) + val releaseName = release.getString("name") + val releaseUrl = release.getString("html_url") + var haveUpdate = releaseName.isNotBlank() + haveUpdate = if (isPreview) { + if (checkPreview) { + haveUpdate && releaseName != BuildConfig.PRE_VERSION_NAME + } else { + // User: 1.3.9 pre-1.4.0 Stable: 1.3.9 -> No update + haveUpdate && releaseName != BuildConfig.VERSION_NAME + } + } else { + // User: 1.4.0 Preview: pre-1.4.0 -> No update + // User: 1.4.0 Preview: pre-1.4.1 -> Update + // User: 1.4.0 Stable: 1.4.0 -> No update + // User: 1.4.0 Stable: 1.4.1 -> Update + haveUpdate && !releaseName.contains(BuildConfig.VERSION_NAME) + } + runOnMainDispatcher { + if (haveUpdate) { + val context = requireContext() + MaterialAlertDialogBuilder(context) + .setTitle(R.string.update_dialog_title) + .setMessage( + context.getString( + R.string.update_dialog_message, + SagerNet.appVersionNameForDisplay, + releaseName + ) + ) + .setPositiveButton(R.string.yes) { _, _ -> + val intent = Intent(Intent.ACTION_VIEW, releaseUrl.toUri()) + context.startActivity(intent) + } + .setNegativeButton(R.string.no, null) + .show() + } else { + Toast.makeText(app, R.string.check_update_no, Toast.LENGTH_SHORT).show() + } + } + } catch (e: Exception) { + Logs.w(e) + runOnMainDispatcher { + Toast.makeText(app, e.readableMessage, Toast.LENGTH_SHORT).show() + } + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt index 436151aae..7f3892430 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt @@ -2,7 +2,6 @@ package io.nekohasekai.sagernet.ui import android.content.Intent import android.content.pm.ApplicationInfo -import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.os.Bundle @@ -45,6 +44,11 @@ import kotlin.coroutines.coroutineContext class AppListActivity : ThemedActivity() { companion object { private const val SWITCH = "switch" + + private val cachedApps + get() = PackageCache.installedPackages.toMutableMap().apply { + remove(BuildConfig.APPLICATION_ID) + } } private class ProxiedApp( @@ -96,7 +100,8 @@ class AppListActivity : ThemedActivity() { var filteredApps = apps suspend fun reload() { - apps = getCachedApps().mapNotNull { (packageName, packageInfo) -> + PackageCache.reload() + apps = cachedApps.mapNotNull { (packageName, packageInfo) -> coroutineContext[Job]!!.ensureActive() packageInfo.applicationInfo?.let { ProxiedApp(packageManager, it, packageName) } }.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) @@ -156,7 +161,7 @@ class AppListActivity : ThemedActivity() { private fun initProxiedUids(str: String = DataStore.routePackages) { proxiedUids.clear() - val apps = getCachedApps() + val apps = cachedApps for (line in str.lineSequence()) { val app = (apps[line] ?: continue) val uid = app.applicationInfo?.uid ?: continue @@ -174,14 +179,12 @@ class AppListActivity : ThemedActivity() { val adapter = binding.list.adapter as AppsAdapter withContext(Dispatchers.IO) { adapter.reload() } adapter.filter.filter(binding.search.text?.toString() ?: "") - binding.list.crossFadeFrom(loading) - } - } - - fun getCachedApps(): MutableMap { - val packages = PackageCache.installedPackages - return packages.toMutableMap().apply { - remove(BuildConfig.APPLICATION_ID) + if (apps.isEmpty()) { + binding.list.visibility = View.GONE + binding.appPlaceholder.root.crossFadeFrom(loading) + } else { + binding.list.crossFadeFrom(loading) + } } } @@ -191,6 +194,14 @@ class AppListActivity : ThemedActivity() { binding = LayoutAppListBinding.inflate(layoutInflater) setContentView(binding.root) + binding.appPlaceholder.openSettings.setOnClickListener { + val intent = + Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = android.net.Uri.fromParts("package", packageName, null) + } + startActivity(intent) + } + setSupportActionBar(binding.toolbar) supportActionBar?.apply { setTitle(R.string.select_apps) @@ -287,17 +298,6 @@ class AppListActivity : ThemedActivity() { } Snackbar.make(binding.list, R.string.action_import_err, Snackbar.LENGTH_LONG).show() } - - R.id.uninstall_all -> { - runOnDefaultDispatcher { - proxiedUids.clear() - DataStore.routePackages = "" - apps = apps.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() })) - onMainDispatcher { - appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH) - } - } - } } return super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt index e212cbe9c..effbc5823 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt @@ -106,6 +106,7 @@ class AppManagerActivity : ThemedActivity() { var filteredApps = apps suspend fun reload() { + PackageCache.reload() apps = cachedApps.mapNotNull { (packageName, packageInfo) -> coroutineContext[Job]!!.ensureActive() packageInfo.applicationInfo?.let { ProxiedApp(packageManager, it, packageName) } @@ -184,7 +185,12 @@ class AppManagerActivity : ThemedActivity() { val adapter = binding.list.adapter as AppsAdapter withContext(Dispatchers.IO) { adapter.reload() } adapter.filter.filter(binding.search.text?.toString() ?: "") - binding.list.crossFadeFrom(loading) + if (apps.isEmpty()) { + binding.list.visibility = View.GONE + binding.appPlaceholder.root.crossFadeFrom(loading) + } else { + binding.list.crossFadeFrom(loading) + } } } @@ -194,6 +200,14 @@ class AppManagerActivity : ThemedActivity() { binding = LayoutAppsBinding.inflate(layoutInflater) setContentView(binding.root) + binding.appPlaceholder.openSettings.setOnClickListener { + val intent = + Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = android.net.Uri.fromParts("package", packageName, null) + } + startActivity(intent) + } + setSupportActionBar(binding.toolbar) supportActionBar?.apply { setTitle(R.string.proxied_apps) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt index d63848289..25f2b6644 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt @@ -211,7 +211,12 @@ class AssetsActivity : ThemedActivity() { val localVersion = if (file.isFile) { if (versionFile.isFile) { - versionFile.readText().trim() + try { + versionFile.readText().trim() + } catch (e: Throwable) { + snackbar(e.readableMessage) + "" + } } else { "Unknown-" + DateFormat.getDateFormat(app).format(Date(file.lastModified())) } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt index 87aa2a034..b439225f8 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt @@ -16,6 +16,7 @@ import com.jakewharton.processphoenix.ProcessPhoenix import io.nekohasekai.sagernet.BuildConfig import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.SagerNet +import io.nekohasekai.sagernet.bg.Executable import io.nekohasekai.sagernet.database.* import io.nekohasekai.sagernet.database.preference.KeyValuePair import io.nekohasekai.sagernet.database.preference.PublicDatabase @@ -23,6 +24,7 @@ import io.nekohasekai.sagernet.databinding.LayoutBackupBinding import io.nekohasekai.sagernet.databinding.LayoutImportBinding import io.nekohasekai.sagernet.databinding.LayoutProgressBinding import io.nekohasekai.sagernet.ktx.* +import kotlinx.coroutines.delay import moe.matsuri.nb4a.utils.Util import org.json.JSONArray import org.json.JSONObject @@ -34,33 +36,45 @@ class BackupFragment : NamedFragment(R.layout.layout_backup) { override fun name0() = app.getString(R.string.backup) var content = "" - private val exportSettings = registerForActivityResult(ActivityResultContracts.CreateDocument()) { data -> - if (data != null) { - runOnDefaultDispatcher { - try { - requireActivity().contentResolver.openOutputStream( - data - )!!.bufferedWriter().use { - it.write(content) - } - onMainDispatcher { - snackbar(getString(R.string.action_export_msg)).show() - } - } catch (e: Exception) { - Logs.w(e) - onMainDispatcher { - snackbar(e.readableMessage).show() + private val exportSettings = + registerForActivityResult(ActivityResultContracts.CreateDocument()) { data -> + if (data != null) { + runOnDefaultDispatcher { + try { + requireActivity().contentResolver.openOutputStream( + data + )!!.bufferedWriter().use { + it.write(content) + } + onMainDispatcher { + snackbar(getString(R.string.action_export_msg)).show() + } + } catch (e: Exception) { + Logs.w(e) + onMainDispatcher { + snackbar(e.readableMessage).show() + } } } - } } - } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val binding = LayoutBackupBinding.bind(view) + + binding.resetSettings.setOnClickListener { + MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.confirm) + .setMessage(R.string.reset_settings_message) + .setNegativeButton(R.string.no, null) + .setPositiveButton(R.string.yes) { _, _ -> + DataStore.configurationStore.reset() + triggerFullRestart(requireContext()) + } + .show() + } + binding.actionExport.setOnClickListener { runOnDefaultDispatcher { content = doBackup( @@ -230,9 +244,7 @@ class BackupFragment : NamedFragment(R.layout.layout_backup) { import.backupRules.isChecked, import.backupSettings.isChecked ) - ProcessPhoenix.triggerRebirth( - requireContext(), Intent(requireContext(), MainActivity::class.java) - ) + triggerFullRestart(requireContext()) }.onFailure { Logs.w(it) onMainDispatcher { diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt index d91524427..054091075 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt @@ -1,8 +1,8 @@ package io.nekohasekai.sagernet.ui +import android.annotation.SuppressLint import android.content.Intent import android.graphics.Color -import android.net.Uri import android.os.Bundle import android.os.SystemClock import android.provider.OpenableColumns @@ -22,6 +22,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar +import androidx.core.net.toUri import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.size @@ -92,12 +93,11 @@ import io.nekohasekai.sagernet.ui.profile.WireGuardSettingsActivity import io.nekohasekai.sagernet.widget.QRCodeDialog import io.nekohasekai.sagernet.widget.UndoSnackbarManager import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import kotlinx.coroutines.newFixedThreadPoolContext import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import moe.matsuri.nb4a.Protocols @@ -105,16 +105,15 @@ import moe.matsuri.nb4a.Protocols.getProtocolColor import moe.matsuri.nb4a.proxy.anytls.AnyTLSSettingsActivity import moe.matsuri.nb4a.proxy.config.ConfigSettingActivity import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSSettingsActivity +import moe.matsuri.nb4a.ui.ConnectionTestNotification import okhttp3.internal.closeQuietly -import java.net.InetAddress import java.net.InetSocketAddress import java.net.Socket import java.net.UnknownHostException -import java.util.Collections +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicInteger import java.util.zip.ZipInputStream -import kotlin.collections.set class ConfigurationFragment @JvmOverloads constructor( val select: Boolean = false, val selectedItem: ProxyEntity? = null, val titleRes: Int = 0 @@ -160,6 +159,7 @@ class ConfigurationFragment @JvmOverloads constructor( override fun onQueryTextSubmit(query: String): Boolean = false + @SuppressLint("DetachAndAttachSameFragment") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -317,7 +317,7 @@ class ConfigurationFragment @JvmOverloads constructor( snackbar(getString(R.string.no_proxies_found_in_file)).show() } else import(proxies) } catch (e: SubscriptionFoundException) { - (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link)) + (requireActivity() as MainActivity).importSubscription(e.link.toUri()) } catch (e: Exception) { Logs.w(e) onMainDispatcher { @@ -360,7 +360,7 @@ class ConfigurationFragment @JvmOverloads constructor( snackbar(getString(R.string.no_proxies_found_in_clipboard)).show() } else import(proxies) } catch (e: SubscriptionFoundException) { - (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link)) + (requireActivity() as MainActivity).importSubscription(e.link.toUri()) } catch (e: Exception) { Logs.w(e) @@ -597,28 +597,41 @@ class ConfigurationFragment @JvmOverloads constructor( inner class TestDialog { val binding = LayoutProgressListBinding.inflate(layoutInflater) val builder = MaterialAlertDialogBuilder(requireContext()).setView(binding.root) - .setNegativeButton(android.R.string.cancel) { _, _ -> - cancel() + .setPositiveButton(R.string.minimize) { _, _ -> + minimize() } - .setOnDismissListener { + .setNegativeButton(android.R.string.cancel) { _, _ -> cancel() } .setCancelable(false) lateinit var cancel: () -> Unit - val fragment by lazy { getCurrentGroupFragment() } - val results = Collections.synchronizedList(mutableListOf()) + lateinit var minimize: () -> Unit + + val dialogStatus = AtomicInteger(0) // 1: hidden 2: cancelled + var notification: ConnectionTestNotification? = null + + val results: MutableSet = ConcurrentHashMap.newKeySet() var proxyN = 0 val finishedN = AtomicInteger(0) - suspend fun insert(profile: ProxyEntity?) { - results.add(profile) - } + fun update(profile: ProxyEntity) { + if (dialogStatus.get() != 2) { + results.add(profile) + } + runOnMainDispatcher { + val context = context ?: return@runOnMainDispatcher + val progress = finishedN.addAndGet(1) + val status = dialogStatus.get() + notification?.updateNotification( + progress, + proxyN, + progress >= proxyN || status == 2 + ) + if (status >= 1) return@runOnMainDispatcher + if (!isAdded) return@runOnMainDispatcher - suspend fun update(profile: ProxyEntity) { - fragment?.configurationListView?.post { - val context = context ?: return@post - if (!isAdded) return@post + // refresh dialog var profileStatusText: String? = null var profileStatusColor = 0 @@ -670,64 +683,46 @@ class ConfigurationFragment @JvmOverloads constructor( } binding.nowTesting.text = text - binding.progress.text = "${finishedN.addAndGet(1)} / $proxyN" + binding.progress.text = "$progress / $proxyN" } } } - fun stopService() { - if (DataStore.serviceState.started) SagerNet.stopService() - } - @OptIn(DelicateCoroutinesApi::class) @Suppress("EXPERIMENTAL_API_USAGE") fun pingTest(icmpPing: Boolean) { + if (DataStore.runningTest) return else DataStore.runningTest = true val test = TestDialog() - val testJobs = mutableListOf() val dialog = test.builder.show() + val testJobs = mutableListOf() + val group = DataStore.currentGroup() + val mainJob = runOnDefaultDispatcher { - if (DataStore.serviceState.started) { - stopService() - delay(500) // wait for service stop - } - val group = DataStore.currentGroup() - val profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id) - test.proxyN = profilesUnfiltered.size - val profiles = ConcurrentLinkedQueue(profilesUnfiltered) - val testPool = newFixedThreadPoolContext( - DataStore.connectionTestConcurrent, - "pingTest" - ) + val profilesList = SagerDatabase.proxyDao.getByGroup(group.id).filter { + if (icmpPing) { + if (it.requireBean().canICMPing()) { + return@filter true + } + } else { + if (it.requireBean().canTCPing()) { + return@filter true + } + } + return@filter false + } + test.proxyN = profilesList.size + val profiles = ConcurrentLinkedQueue(profilesList) repeat(DataStore.connectionTestConcurrent) { - testJobs.add(launch(testPool) { + testJobs.add(launch(Dispatchers.IO) { while (isActive) { val profile = profiles.poll() ?: break - if (icmpPing) { - if (!profile.requireBean().canICMPing()) { - profile.status = -1 - profile.error = - app.getString(R.string.connection_test_icmp_ping_unavailable) - test.insert(profile) - continue - } - } else { - if (!profile.requireBean().canTCPing()) { - profile.status = -1 - profile.error = - app.getString(R.string.connection_test_tcp_ping_unavailable) - test.insert(profile) - continue - } - } - profile.status = 0 - test.insert(profile) var address = profile.requireBean().serverAddress if (!address.isIpAddress()) { try { - InetAddress.getAllByName(address).apply { + SagerNet.underlyingNetwork!!.getAllByName(address).apply { if (isNotEmpty()) { address = this[0].hostAddress } @@ -746,7 +741,9 @@ class ConfigurationFragment @JvmOverloads constructor( if (icmpPing) { // removed } else { - val socket = Socket() + val socket = + SagerNet.underlyingNetwork?.socketFactory?.createSocket() + ?: Socket() try { socket.soTimeout = 3000 socket.bind(InetSocketAddress(0)) @@ -802,15 +799,18 @@ class ConfigurationFragment @JvmOverloads constructor( } testJobs.joinAll() - testPool.close() - onMainDispatcher { - dialog.dismiss() + runOnMainDispatcher { + test.cancel() } } test.cancel = { + test.dialogStatus.set(2) + dialog.dismiss() runOnDefaultDispatcher { - test.results.filterNotNull().forEach { + mainJob.cancel() + testJobs.forEach { it.cancel() } + test.results.forEach { try { ProfileManager.updateProfile(it) } catch (e: Exception) { @@ -818,34 +818,37 @@ class ConfigurationFragment @JvmOverloads constructor( } } GroupManager.postReload(DataStore.currentGroupId()) - mainJob.cancel() - testJobs.forEach { it.cancel() } + DataStore.runningTest = false } } + test.minimize = { + test.dialogStatus.set(1) + test.notification = ConnectionTestNotification( + dialog.context, + "[${group.displayName()}] ${getString(R.string.connection_test)}" + ) + dialog.hide() + } } @OptIn(DelicateCoroutinesApi::class) fun urlTest() { + if (DataStore.runningTest) return else DataStore.runningTest = true val test = TestDialog() val dialog = test.builder.show() val testJobs = mutableListOf() + val group = DataStore.currentGroup() val mainJob = runOnDefaultDispatcher { - val group = DataStore.currentGroup() - val profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id) - test.proxyN = profilesUnfiltered.size - val profiles = ConcurrentLinkedQueue(profilesUnfiltered) - val testPool = newFixedThreadPoolContext( - DataStore.connectionTestConcurrent, - "urlTest" - ) + val profilesList = SagerDatabase.proxyDao.getByGroup(group.id) + test.proxyN = profilesList.size + val profiles = ConcurrentLinkedQueue(profilesList) repeat(DataStore.connectionTestConcurrent) { - testJobs.add(launch(testPool) { + testJobs.add(launch(Dispatchers.IO) { val urlTest = UrlTest() // note: this is NOT in bg process while (isActive) { val profile = profiles.poll() ?: break profile.status = 0 - test.insert(profile) try { val result = urlTest.doTest(profile) @@ -866,13 +869,17 @@ class ConfigurationFragment @JvmOverloads constructor( testJobs.joinAll() - onMainDispatcher { - dialog.dismiss() + runOnMainDispatcher { + test.cancel() } } test.cancel = { + test.dialogStatus.set(2) + dialog.dismiss() runOnDefaultDispatcher { - test.results.filterNotNull().forEach { + mainJob.cancel() + testJobs.forEach { it.cancel() } + test.results.forEach { try { ProfileManager.updateProfile(it) } catch (e: Exception) { @@ -880,10 +887,17 @@ class ConfigurationFragment @JvmOverloads constructor( } } GroupManager.postReload(DataStore.currentGroupId()) - mainJob.cancel() - testJobs.forEach { it.cancel() } + DataStore.runningTest = false } } + test.minimize = { + test.dialogStatus.set(1) + test.notification = ConnectionTestNotification( + dialog.context, + "[${group.displayName()}] ${getString(R.string.connection_test)}" + ) + dialog.hide() + } } inner class GroupPagerAdapter : FragmentStateAdapter(this), @@ -1412,7 +1426,6 @@ class ConfigurationFragment @JvmOverloads constructor( fun reloadProfiles() { var newProfiles = SagerDatabase.proxyDao.getByGroup(proxyGroup.id) - val subscription = proxyGroup.subscription when (proxyGroup.order) { GroupOrder.BY_NAME -> { newProfiles = newProfiles.sortedBy { it.displayName() } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt deleted file mode 100644 index a5d33a35f..000000000 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.nekohasekai.sagernet.ui - -import android.os.Bundle -import android.view.View -import io.nekohasekai.sagernet.R -import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.databinding.LayoutDebugBinding -import io.nekohasekai.sagernet.ktx.snackbar - -class DebugFragment : NamedFragment(R.layout.layout_debug) { - - override fun name0() = "Debug" - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val binding = LayoutDebugBinding.bind(view) - - binding.debugCrash.setOnClickListener { - error("test crash") - } - binding.resetSettings.setOnClickListener { - DataStore.configurationStore.reset() - snackbar("Cleared").show() - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt index ffc1f55ba..840234b54 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt @@ -476,12 +476,17 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group), used += toLong() } val total = get("total=([0-9]+)")?.toLong() ?: 0 + val remain = total - used if (used > 0 || total > 0) { - text += getString( - R.string.subscription_traffic, - used.toBytesString(), - (total - used).toBytesString() - ) + text += if (remain > 0) { + getString( + R.string.subscription_traffic, + used.toBytesString(), + remain.toBytesString() + ) + } else { + getString(R.string.subscription_used, used.toBytesString()) + } } get("expire=([0-9]+)")?.apply { text += "\n" diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt index ef22f30b4..2f276ee9f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt @@ -9,9 +9,10 @@ import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE import android.text.style.ForegroundColorSpan import android.view.MenuItem import android.view.View -import android.widget.ScrollView +import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import androidx.core.view.ViewCompat +import androidx.core.view.doOnLayout import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.databinding.LayoutLogcatBinding import io.nekohasekai.sagernet.ktx.* @@ -41,7 +42,6 @@ class LogcatFragment : ToolbarFragment(R.layout.layout_logcat), ViewCompat.setOnApplyWindowInsetsListener(binding.root, ListListener) reloadSession() - // TODO new logcat } private fun getColorForLine(line: String): ForegroundColorSpan { @@ -75,9 +75,10 @@ class LogcatFragment : ToolbarFragment(R.layout.layout_logcat), offset += line.length + 1 } binding.textview.text = span - - binding.scroolview.post { - binding.scroolview.fullScroll(ScrollView.FOCUS_DOWN) + binding.textview.clearFocus() + // 等 textview 完成最终 layout 再滚动到底部 + binding.textview.doOnLayout { + binding.scroolview.scrollTo(0, binding.textview.height) } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt index e8a17fc50..a60012e10 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt @@ -18,6 +18,7 @@ import androidx.preference.PreferenceDataStore import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar +import io.nekohasekai.sagernet.BuildConfig import io.nekohasekai.sagernet.GroupType import io.nekohasekai.sagernet.Key import io.nekohasekai.sagernet.R @@ -41,6 +42,7 @@ import io.nekohasekai.sagernet.group.GroupInterfaceAdapter import io.nekohasekai.sagernet.group.GroupUpdater import io.nekohasekai.sagernet.ktx.alert import io.nekohasekai.sagernet.ktx.isPlay +import io.nekohasekai.sagernet.ktx.isPreview import io.nekohasekai.sagernet.ktx.launchCustomTab import io.nekohasekai.sagernet.ktx.onMainDispatcher import io.nekohasekai.sagernet.ktx.parseProxies @@ -114,6 +116,14 @@ class MainActivity : ThemedActivity(), ) } } + + if (isPreview) { + MaterialAlertDialogBuilder(this) + .setTitle(BuildConfig.PRE_VERSION_NAME) + .setMessage(R.string.preview_version_hint) + .setPositiveButton(android.R.string.ok, null) + .show() + } } fun refreshNavMenu(clashApi: Boolean) { diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt index 6ef29c114..b8bb94145 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt @@ -3,18 +3,9 @@ package io.nekohasekai.sagernet.ui import android.content.Intent import android.os.Bundle import android.view.View -import androidx.appcompat.app.AlertDialog import io.nekohasekai.sagernet.R -import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.database.ProfileManager import io.nekohasekai.sagernet.databinding.LayoutNetworkBinding -import io.nekohasekai.sagernet.databinding.LayoutProgressBinding -import io.nekohasekai.sagernet.ktx.* -import io.nekohasekai.sagernet.utils.Cloudflare -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.runBlocking +import io.nekohasekai.sagernet.ktx.app class NetworkFragment : NamedFragment(R.layout.layout_network) { @@ -27,58 +18,6 @@ class NetworkFragment : NamedFragment(R.layout.layout_network) { binding.stunTest.setOnClickListener { startActivity(Intent(requireContext(), StunActivity::class.java)) } - - //Markwon.create(requireContext()) - // .setMarkdown(binding.wrapLicense, getString(R.string.warp_license)) - - binding.warpGenerate.setOnClickListener { - runBlocking { - generateWarpConfiguration() - } - } - } - - suspend fun generateWarpConfiguration() { - val activity = requireActivity() as MainActivity - val binding = LayoutProgressBinding.inflate(layoutInflater).apply { - content.setText(R.string.generating) - } - var job: Job? = null - val dialog = AlertDialog.Builder(requireContext()) - .setView(binding.root) - .setCancelable(false) - .setNegativeButton(android.R.string.cancel) { _, _ -> - job?.cancel() - } - .show() - job = runOnDefaultDispatcher { - try { - val bean = Cloudflare.makeWireGuardConfiguration() - if (isActive) { - val groupId = DataStore.selectedGroupForImport() - if (DataStore.selectedGroup != groupId) { - DataStore.selectedGroup = groupId - } - onMainDispatcher { - activity.displayFragmentWithId(R.id.nav_configuration) - } - delay(1000L) - onMainDispatcher { - dialog.dismiss() - } - ProfileManager.createProfile(groupId, bean) - } - } catch (e: Exception) { - Logs.w(e) - onMainDispatcher { - if (isActive) { - dialog.dismiss() - activity.snackbar(e.readableMessage).show() - } - } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/QuickDisableShortcut.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/QuickDisableShortcut.kt new file mode 100644 index 000000000..9571a75c8 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/QuickDisableShortcut.kt @@ -0,0 +1,58 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv * + * Copyright (C) 2017 by Mygod Studio <[Email1]> * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package io.nekohasekai.sagernet.ui + +import android.app.Activity +import android.content.pm.ShortcutManager +import android.os.Build +import android.os.Bundle +import androidx.core.content.getSystemService +import io.nekohasekai.sagernet.SagerNet +import io.nekohasekai.sagernet.aidl.ISagerNetService +import io.nekohasekai.sagernet.bg.BaseService +import io.nekohasekai.sagernet.bg.SagerConnection + +class QuickDisableShortcut : Activity(), SagerConnection.Callback { + private val connection = SagerConnection(SagerConnection.CONNECTION_ID_SHORTCUT) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + connection.connect(this, this) + if (Build.VERSION.SDK_INT >= 25) { + getSystemService()!!.reportShortcutUsed("disable") + } + } + + override fun onServiceConnected(service: ISagerNetService) { + val state = BaseService.State.values()[service.state] + if (state.canStop) { + SagerNet.stopService() + } + finish() + } + + override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {} + + override fun onDestroy() { + connection.disconnect(this) + super.onDestroy() + } +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/QuickEnableShortcut.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/QuickEnableShortcut.kt new file mode 100644 index 000000000..590868d18 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/QuickEnableShortcut.kt @@ -0,0 +1,58 @@ +/******************************************************************************* + * * + * Copyright (C) 2017 by Max Lv <[Email0]> * + * Copyright (C) 2017 by Mygod Studio <[Email1]> * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +package io.nekohasekai.sagernet.ui + +import android.app.Activity +import android.content.pm.ShortcutManager +import android.os.Build +import android.os.Bundle +import androidx.core.content.getSystemService +import io.nekohasekai.sagernet.SagerNet +import io.nekohasekai.sagernet.aidl.ISagerNetService +import io.nekohasekai.sagernet.bg.BaseService +import io.nekohasekai.sagernet.bg.SagerConnection + +class QuickEnableShortcut : Activity(), SagerConnection.Callback { + private val connection = SagerConnection(SagerConnection.CONNECTION_ID_SHORTCUT) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + connection.connect(this, this) + if (Build.VERSION.SDK_INT >= 25) { + getSystemService()!!.reportShortcutUsed("enable") + } + } + + override fun onServiceConnected(service: ISagerNetService) { + val state = BaseService.State.values()[service.state] + if (state == BaseService.State.Stopped) { + SagerNet.startService() + } + finish() + } + + override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {} + + override fun onDestroy() { + connection.disconnect(this) + super.onDestroy() + } +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt index 825d24ae5..aaa062de3 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt @@ -39,6 +39,7 @@ import io.nekohasekai.sagernet.widget.AppListPreference import io.nekohasekai.sagernet.widget.ListListener import io.nekohasekai.sagernet.widget.OutboundPreference import kotlinx.parcelize.Parcelize +import moe.matsuri.nb4a.ui.EditConfigPreference @Suppress("UNCHECKED_CAST") class RouteSettingsActivity( @@ -57,6 +58,7 @@ class RouteSettingsActivity( fun RuleEntity.init() { DataStore.routeName = name + DataStore.serverConfig = config DataStore.routeDomain = domains DataStore.routeIP = ip DataStore.routePort = port @@ -76,6 +78,7 @@ class RouteSettingsActivity( fun RuleEntity.serialize() { name = DataStore.routeName + config = DataStore.serverConfig domains = DataStore.routeDomain ip = DataStore.routeIP port = DataStore.routePort @@ -96,12 +99,10 @@ class RouteSettingsActivity( } } + private lateinit var editConfigPreference: EditConfigPreference + fun needSave(): Boolean { - if (!DataStore.dirty) return false - if (DataStore.routePackages.isBlank() && DataStore.routeDomain.isBlank() && DataStore.routeIP.isBlank() && DataStore.routePort.isBlank() && DataStore.routeSourcePort.isBlank() && DataStore.routeNetwork.isBlank() && DataStore.routeSource.isBlank() && DataStore.routeProtocol.isBlank()) { - return false - } - return true + return DataStore.dirty } fun PreferenceFragmentCompat.createPreferences( @@ -109,6 +110,16 @@ class RouteSettingsActivity( rootKey: String?, ) { addPreferencesFromResource(R.xml.route_preferences) + + editConfigPreference = findPreference(Key.SERVER_CONFIG)!! + } + + override fun onResume() { + super.onResume() + + if (::editConfigPreference.isInitialized) { + editConfigPreference.notifyChanged() + } } val selectProfileForAdd = registerForActivityResult( @@ -163,7 +174,7 @@ class RouteSettingsActivity( } } - fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean { + fun displayPreferenceDialog(preference: Preference): Boolean { return false } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt index 8cda8693c..99f6f6468 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt @@ -4,7 +4,6 @@ import android.Manifest import android.content.Intent import android.content.pm.ShortcutManager import android.graphics.ImageDecoder -import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.MediaStore @@ -13,6 +12,7 @@ import android.view.MenuItem import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.getSystemService +import androidx.core.net.toUri import com.google.zxing.Result import com.king.zxing.CameraScan import com.king.zxing.DefaultCameraScan @@ -138,7 +138,7 @@ class ScannerActivity : ThemedActivity(), } catch (e: SubscriptionFoundException) { startActivity(Intent(this@ScannerActivity, MainActivity::class.java).apply { action = Intent.ACTION_VIEW - data = Uri.parse(e.link) + data = e.link.toUri() }) } catch (e: Throwable) { Logs.w(e) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt index 5c91718c9..18d645455 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt @@ -22,6 +22,9 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { private lateinit var isProxyApps: SwitchPreference + private lateinit var globalCustomConfig: EditConfigPreference + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -77,6 +80,8 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { val logLevel = findPreference(Key.LOG_LEVEL)!! val mtu = findPreference(Key.MTU)!! + globalCustomConfig = findPreference(Key.GLOBAL_CUSTOM_CONFIG)!! + globalCustomConfig.useConfigStore(Key.GLOBAL_CUSTOM_CONFIG) logLevel.dialogLayoutResource = R.layout.layout_loglevel_help logLevel.setOnPreferenceChangeListener { _, _ -> @@ -162,7 +167,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { resolveDestination.onPreferenceChangeListener = reloadListener tunImplementation.onPreferenceChangeListener = reloadListener acquireWakeLock.onPreferenceChangeListener = reloadListener - + globalCustomConfig.onPreferenceChangeListener = reloadListener } override fun onResume() { @@ -171,6 +176,9 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { if (::isProxyApps.isInitialized) { isProxyApps.isChecked = DataStore.proxyApps } + if (::globalCustomConfig.isInitialized) { + globalCustomConfig.notifyChanged() + } } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt index bf73ca1d9..32738ea98 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt @@ -7,7 +7,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import com.google.android.material.tabs.TabLayoutMediator import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.databinding.LayoutToolsBinding -import io.nekohasekai.sagernet.ktx.isExpert class ToolsFragment : ToolbarFragment(R.layout.layout_tools) { @@ -19,8 +18,6 @@ class ToolsFragment : ToolbarFragment(R.layout.layout_tools) { tools.add(NetworkFragment()) tools.add(BackupFragment()) - if (isExpert) tools.add(DebugFragment()) - val binding = LayoutToolsBinding.bind(view) binding.toolsPager.adapter = ToolsAdapter(tools) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt index faec56e05..e144a26ef 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt @@ -5,8 +5,12 @@ import android.content.DialogInterface import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.ViewGroup.MarginLayoutParams +import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams import androidx.core.widget.addTextChangedListener import com.blacksquircle.ui.editorkit.insert import com.blacksquircle.ui.language.json.JsonLanguage @@ -17,7 +21,9 @@ import io.nekohasekai.sagernet.Key import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.databinding.LayoutEditConfigBinding -import io.nekohasekai.sagernet.ktx.* +import io.nekohasekai.sagernet.ktx.getColorAttr +import io.nekohasekai.sagernet.ktx.readableMessage +import io.nekohasekai.sagernet.ktx.toStringPretty import io.nekohasekai.sagernet.ui.ThemedActivity import io.nekohasekai.sagernet.widget.ListListener import moe.matsuri.nb4a.ui.ExtendedKeyboard @@ -27,6 +33,7 @@ class ConfigEditActivity : ThemedActivity() { var dirty = false var key = Key.SERVER_CONFIG + var useConfigStore = false class UnsavedChangesDialogFragment : AlertDialogFragment() { override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) { @@ -49,6 +56,7 @@ class ConfigEditActivity : ThemedActivity() { intent?.extras?.apply { getString("key")?.let { key = it } + getString("useConfigStore")?.let { useConfigStore = true } } binding = LayoutEditConfigBinding.inflate(layoutInflater) @@ -64,7 +72,11 @@ class ConfigEditActivity : ThemedActivity() { binding.editor.apply { language = JsonLanguage() setHorizontallyScrolling(true) - setTextContent(DataStore.profileCacheStore.getString(key)!!) + if (useConfigStore) { + setTextContent(DataStore.configurationStore.getString(key) ?: "") + } else { + setTextContent(DataStore.profileCacheStore.getString(key) ?: "") + } addTextChangedListener { if (!dirty) { dirty = true @@ -74,7 +86,10 @@ class ConfigEditActivity : ThemedActivity() { } binding.actionTab.setOnClickListener { - binding.editor.insert(binding.editor.tab()) + try { + binding.editor.insert(binding.editor.tab()) + } catch (e: Exception) { + } } binding.actionUndo.setOnClickListener { try { @@ -95,11 +110,33 @@ class ConfigEditActivity : ThemedActivity() { } val extendedKeyboard = findViewById(R.id.extended_keyboard) - extendedKeyboard.setKeyListener { char -> binding.editor.insert(char) } + extendedKeyboard.setKeyListener { char -> + try { + binding.editor.insert(char) + } catch (e: Exception) { + } + } extendedKeyboard.setHasFixedSize(true) extendedKeyboard.submitList("{},:_\"".map { it.toString() }) extendedKeyboard.setBackgroundColor(getColorAttr(R.attr.primaryOrTextPrimary)) + val keyboardContainer = findViewById(R.id.keyboard_container) + ViewCompat.setOnApplyWindowInsetsListener(keyboardContainer) { v, windowInsets -> + val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()) + val systemBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime()) + v.updateLayoutParams { + // systemBar insets are applied to the bottom of the keyboard + if (imeVisible) { + bottomMargin = imeInsets.bottom - systemBarInsets.bottom + } else { + bottomMargin = 0 + } + } + + WindowInsetsCompat.CONSUMED + } + ViewCompat.setOnApplyWindowInsetsListener(binding.root, ListListener) } @@ -119,7 +156,11 @@ class ConfigEditActivity : ThemedActivity() { fun saveAndExit() { formatText()?.let { - DataStore.profileCacheStore.putString(key, it) + if (useConfigStore) { + DataStore.configurationStore.putString(key, it) + } else { + DataStore.profileCacheStore.putString(key, it) + } finish() } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt deleted file mode 100644 index b70d854a0..000000000 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt +++ /dev/null @@ -1,74 +0,0 @@ -package io.nekohasekai.sagernet.utils - -import com.wireguard.crypto.KeyPair -import io.nekohasekai.sagernet.database.DataStore -import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean -import io.nekohasekai.sagernet.ktx.Logs -import io.nekohasekai.sagernet.utils.cf.DeviceResponse -import io.nekohasekai.sagernet.utils.cf.RegisterRequest -import io.nekohasekai.sagernet.utils.cf.UpdateDeviceRequest -import libcore.Libcore -import moe.matsuri.nb4a.utils.JavaUtil.gson -import moe.matsuri.nb4a.utils.Util - -// kang from wgcf -object Cloudflare { - - private const val API_URL = "https://api.cloudflareclient.com" - private const val API_VERSION = "v0a1922" - - private const val CLIENT_VERSION_KEY = "CF-Client-Version" - private const val CLIENT_VERSION = "a-6.3-1922" - - fun makeWireGuardConfiguration(): WireGuardBean { - val keyPair = KeyPair() - val client = Libcore.newHttpClient().apply { - pinnedTLS12() - trySocks5(DataStore.mixedPort) - } - - try { - val response = client.newRequest().apply { - setMethod("POST") - setURL("$API_URL/$API_VERSION/reg") - setHeader(CLIENT_VERSION_KEY, CLIENT_VERSION) - setHeader("Accept", "application/json") - setHeader("Content-Type", "application/json") - setContentString(RegisterRequest.newRequest(keyPair.publicKey)) - setUserAgent("okhttp/3.12.1") - }.execute() - - Logs.d(Util.getStringBox(response.contentString)) - val device = - gson.fromJson(Util.getStringBox(response.contentString), DeviceResponse::class.java) - val accessToken = device.token - - client.newRequest().apply { - setMethod("PATCH") - setURL(API_URL + "/" + API_VERSION + "/reg/" + device.id + "/account/reg/" + device.id) - setHeader("Accept", "application/json") - setHeader("Content-Type", "application/json") - setHeader("Authorization", "Bearer $accessToken") - setHeader(CLIENT_VERSION_KEY, CLIENT_VERSION) - setContentString(UpdateDeviceRequest.newRequest()) - setUserAgent("okhttp/3.12.1") - }.execute() - - val peer = device.config.peers[0] - val localAddresses = device.config.interfaceX.addresses - return WireGuardBean().apply { - name = "CloudFlare Warp ${device.account.id}" - privateKey = keyPair.privateKey.toBase64() - peerPublicKey = peer.publicKey - serverAddress = peer.endpoint.host.substringBeforeLast(":") - serverPort = peer.endpoint.host.substringAfterLast(":").toInt() - localAddress = localAddresses.v4 + "/32" + "\n" + localAddresses.v6 + "/128" - mtu = 1280 - reserved = device.config.clientId - } - } finally { - client.close() - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt index 292f929dc..2aa93fac7 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt @@ -6,6 +6,7 @@ import android.os.Build import android.util.Log import com.jakewharton.processphoenix.ProcessPhoenix import io.nekohasekai.sagernet.BuildConfig +import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.database.preference.PublicDatabase import io.nekohasekai.sagernet.ktx.Logs import io.nekohasekai.sagernet.ktx.app @@ -61,7 +62,7 @@ object CrashHandler : Thread.UncaughtExceptionHandler { fun buildReportHeader(): String { var report = "" - report += "NekoBox for Andoird ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) ${BuildConfig.FLAVOR.uppercase()}\n" + report += "NekoBox for Android ${SagerNet.appVersionNameForDisplay} (${BuildConfig.VERSION_CODE})\n" report += "Date: ${getCurrentMilliSecondUTCTimeStamp()}\n\n" report += "OS_VERSION: ${getSystemPropertyWithAndroidAPI("os.version")}\n" report += "SDK_INT: ${Build.VERSION.SDK_INT}\n" @@ -102,7 +103,7 @@ object CrashHandler : Thread.UncaughtExceptionHandler { report += "\n" report += pair.key + ": " + pair.toString() } - }catch (e: Exception) { + } catch (e: Exception) { report += "Export settings failed: " + formatThrowable(e) } @@ -136,7 +137,8 @@ object CrashHandler : Thread.UncaughtExceptionHandler { if (matcher.matches()) { key = matcher.group(1) value = matcher.group(2) - if (key != null && value != null && !key.isEmpty() && !value.isEmpty()) systemProperties[key] = value + if (key != null && value != null && !key.isEmpty() && !value.isEmpty()) systemProperties[key] = + value } } bufferedReader.close() diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt index a2e09ef58..c5e9370c7 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt @@ -67,15 +67,6 @@ object PackageCache { operator fun get(uid: Int) = uidMap[uid] operator fun get(packageName: String) = packageMap[packageName] - suspend fun awaitLoad() { - if (::packageMap.isInitialized) { - return - } - loaded.withLock { - // just await - } - } - fun awaitLoadSync() { if (::packageMap.isInitialized) { return diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/DeviceResponse.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/cf/DeviceResponse.kt deleted file mode 100644 index 874304dc2..000000000 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/DeviceResponse.kt +++ /dev/null @@ -1,114 +0,0 @@ -package io.nekohasekai.sagernet.utils.cf - - -import com.google.gson.annotations.SerializedName - -data class DeviceResponse( - @SerializedName("created") - var created: String = "", - @SerializedName("type") - var type: String = "", - @SerializedName("locale") - var locale: String = "", - @SerializedName("enabled") - var enabled: Boolean = false, - @SerializedName("token") - var token: String = "", - @SerializedName("waitlist_enabled") - var waitlistEnabled: Boolean = false, - @SerializedName("install_id") - var installId: String = "", - @SerializedName("warp_enabled") - var warpEnabled: Boolean = false, - @SerializedName("name") - var name: String = "", - @SerializedName("fcm_token") - var fcmToken: String = "", - @SerializedName("tos") - var tos: String = "", - @SerializedName("model") - var model: String = "", - @SerializedName("id") - var id: String = "", - @SerializedName("place") - var place: Int = 0, - @SerializedName("config") - var config: Config = Config(), - @SerializedName("updated") - var updated: String = "", - @SerializedName("key") - var key: String = "", - @SerializedName("account") - var account: Account = Account() -) { - data class Config( - @SerializedName("peers") - var peers: List = listOf(), - @SerializedName("services") - var services: Services = Services(), - @SerializedName("interface") - var interfaceX: Interface = Interface(), - @SerializedName("client_id") - var clientId: String = "" - ) { - data class Peer( - @SerializedName("public_key") - var publicKey: String = "", - @SerializedName("endpoint") - var endpoint: Endpoint = Endpoint() - ) { - data class Endpoint( - @SerializedName("v6") - var v6: String = "", - @SerializedName("host") - var host: String = "", - @SerializedName("v4") - var v4: String = "" - ) - } - - data class Services( - @SerializedName("http_proxy") - var httpProxy: String = "" - ) - - data class Interface( - @SerializedName("addresses") - var addresses: Addresses = Addresses() - ) { - data class Addresses( - @SerializedName("v6") - var v6: String = "", - @SerializedName("v4") - var v4: String = "" - ) - } - } - - data class Account( - @SerializedName("account_type") - var accountType: String = "", - @SerializedName("role") - var role: String = "", - @SerializedName("referral_renewal_countdown") - var referralRenewalCountdown: Int = 0, - @SerializedName("created") - var created: String = "", - @SerializedName("usage") - var usage: Int = 0, - @SerializedName("warp_plus") - var warpPlus: Boolean = false, - @SerializedName("referral_count") - var referralCount: Int = 0, - @SerializedName("license") - var license: String = "", - @SerializedName("quota") - var quota: Int = 0, - @SerializedName("premium_data") - var premiumData: Int = 0, - @SerializedName("id") - var id: String = "", - @SerializedName("updated") - var updated: String = "" - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/RegisterRequest.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/cf/RegisterRequest.kt deleted file mode 100644 index 34bbe7c16..000000000 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/RegisterRequest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.nekohasekai.sagernet.utils.cf - -import com.google.gson.Gson -import com.google.gson.annotations.SerializedName -import com.wireguard.crypto.Key -import java.text.SimpleDateFormat -import java.util.* - -data class RegisterRequest( - @SerializedName("fcm_token") var fcmToken: String = "", - @SerializedName("install_id") var installedId: String = "", - var key: String = "", - var locale: String = "", - var model: String = "", - var tos: String = "", - var type: String = "" -) { - - companion object { - fun newRequest(publicKey: Key): String { - val request = RegisterRequest() - request.fcmToken = "" - request.installedId = "" - request.key = publicKey.toBase64() - request.locale = "en_US" - request.model = "PC" - val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'000000'+08:00", Locale.US) - request.tos = format.format(Date()) - request.type = "Android" - return Gson().toJson(request) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/UpdateDeviceRequest.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/cf/UpdateDeviceRequest.kt deleted file mode 100644 index e12915b41..000000000 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/UpdateDeviceRequest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.nekohasekai.sagernet.utils.cf - -import com.google.gson.Gson - -data class UpdateDeviceRequest( - var name: String, var active: Boolean -) { - companion object { - fun newRequest(name: String = "SagerNet Client", active: Boolean = true) = - Gson().toJson(UpdateDeviceRequest(name, active)) - } -} \ No newline at end of file diff --git a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java index 82572f574..05f89275f 100644 --- a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java +++ b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java @@ -1,19 +1,97 @@ package moe.matsuri.nb4a; -import static moe.matsuri.nb4a.utils.JavaUtil.gson; - +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.ToNumberPolicy; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; import java.util.List; import java.util.Map; +import moe.matsuri.nb4a.utils.Util; + public class SingBoxOptions { // base + private static final Gson gsonSingbox = new GsonBuilder() + .registerTypeHierarchyAdapter(SingBoxOption.class, new SingBoxOptionSerializer()) + .setPrettyPrinting() + .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .setLenient() + .disableHtmlEscaping() + .create(); + public static class SingBoxOption { + + public transient Map _hack_config_map; // 仍然用普通json方式合并,所以Object内不要使用 _hack + + public transient String _hack_custom_config; + + public SingBoxOption() { + _hack_config_map = new HashMap<>(); + } + public Map asMap() { - return gson.fromJson(gson.toJson(this), Map.class); + return gsonSingbox.fromJson(gsonSingbox.toJson(this), Map.class); + } + + } + + public static final class CustomSingBoxOption extends SingBoxOption { + + public transient String config; + + public CustomSingBoxOption(String config) { + super(); + this.config = config; + } + + public Map getBasicMap() { + Map map = gsonSingbox.fromJson(config, Map.class); + if (map == null) { + map = new HashMap<>(); + } + return map; + } + } + + // 自定义序列化器 + public static class SingBoxOptionSerializer implements JsonSerializer { + @Override + public JsonElement serialize(SingBoxOption src, Type typeOfSrc, JsonSerializationContext context) { + // 拿到原始的 delegate(默认序列化器) + TypeAdapter delegate = gsonSingbox.getDelegateAdapter( + new TypeAdapterFactory() { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + return null; // 返回 null,表示只作为“跳过当前自定义”的 marker + } + }, + TypeToken.get(src.getClass()) + ); + Map map; + if (src instanceof CustomSingBoxOption) { + map = ((CustomSingBoxOption) src).getBasicMap(); + } else { + map = gsonSingbox.fromJson(((TypeAdapter) delegate).toJson(src), Map.class); + } + if (src._hack_config_map != null && !src._hack_config_map.isEmpty()) { + Util.INSTANCE.mergeMap(map, src._hack_config_map); + } + if (src._hack_custom_config != null && !src._hack_custom_config.isBlank()) { + Util.INSTANCE.mergeJSON(map, src._hack_custom_config); + } + return gsonSingbox.toJsonTree(map); } } @@ -33,7 +111,7 @@ public static class MyOptions extends SingBoxOption { public List inbounds; - public List> outbounds; + public List outbounds; public RouteOptions route; diff --git a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt index 19f733bdd..d3971b2b2 100644 --- a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt +++ b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt @@ -157,5 +157,7 @@ fun SingBoxOptions.Rule_DefaultOptions.checkEmpty(): Boolean { if (port?.isNotEmpty() == true) return false if (port_range?.isNotEmpty() == true) return false if (source_ip_cidr?.isNotEmpty() == true) return false + // + if (!_hack_custom_config.isNullOrBlank()) return false return true } diff --git a/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt b/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt index 27c08e310..9b125aeea 100644 --- a/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt +++ b/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt @@ -6,16 +6,14 @@ import android.os.CancellationSignal import android.system.ErrnoException import androidx.annotation.RequiresApi import io.nekohasekai.sagernet.SagerNet -import io.nekohasekai.sagernet.ktx.tryResumeWithException +import io.nekohasekai.sagernet.ktx.Logs +import io.nekohasekai.sagernet.ktx.runOnIoDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.runBlocking import libcore.ExchangeContext import libcore.LocalDNSTransport import java.net.InetAddress import java.net.UnknownHostException -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine object LocalResolverImpl : LocalDNSTransport { @@ -27,110 +25,126 @@ object LocalResolverImpl : LocalDNSTransport { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q } + override fun networkHandle(): Long { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return SagerNet.underlyingNetwork?.networkHandle ?: 0 + } + return 0 + } + @RequiresApi(Build.VERSION_CODES.Q) override fun exchange(ctx: ExchangeContext, message: ByteArray) { - return runBlocking { - suspendCoroutine { continuation -> - val signal = CancellationSignal() - ctx.onCancel(signal::cancel) - val callback = object : DnsResolver.Callback { - override fun onAnswer(answer: ByteArray, rcode: Int) { - // exchange don't generate rcode error - ctx.rawSuccess(answer) - continuation.resume(Unit) - } + val signal = CancellationSignal() + ctx.onCancel(signal::cancel) - override fun onError(error: DnsResolver.DnsException) { - when (val cause = error.cause) { - is ErrnoException -> { - ctx.errnoCode(cause.errno) - continuation.resume(Unit) - return - } - } - continuation.tryResumeWithException(error) - } + val callback = object : DnsResolver.Callback { + override fun onAnswer(answer: ByteArray, rcode: Int) { + ctx.rawSuccess(answer) + } + + override fun onError(error: DnsResolver.DnsException) { + val cause = error.cause + if (cause is ErrnoException) { + ctx.errnoCode(cause.errno) + } else { + Logs.w(error) + ctx.errnoCode(114514) } - DnsResolver.getInstance().rawQuery( - SagerNet.underlyingNetwork, - message, - DnsResolver.FLAG_NO_RETRY, - Dispatchers.IO.asExecutor(), - signal, - callback - ) } } + + DnsResolver.getInstance().rawQuery( + SagerNet.underlyingNetwork, + message, + DnsResolver.FLAG_NO_RETRY, + Dispatchers.IO.asExecutor(), + signal, + callback + ) } override fun lookup(ctx: ExchangeContext, network: String, domain: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return runBlocking { - suspendCoroutine { continuation -> - val signal = CancellationSignal() - ctx.onCancel(signal::cancel) - val callback = object : DnsResolver.Callback> { - override fun onAnswer(answer: Collection, rcode: Int) { - if (rcode == 0) { - ctx.success((answer as Collection).mapNotNull { it?.hostAddress } - .joinToString("\n")) - } else { - ctx.errorCode(rcode) - } - continuation.resume(Unit) - } + val signal = CancellationSignal() + ctx.onCancel(signal::cancel) - override fun onError(error: DnsResolver.DnsException) { - when (val cause = error.cause) { - is ErrnoException -> { - ctx.errnoCode(cause.errno) - continuation.resume(Unit) - return - } - } - continuation.tryResumeWithException(error) + val callback = object : DnsResolver.Callback> { + override fun onAnswer(answer: Collection, rcode: Int) { + try { + if (rcode == 0) { + ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n")) + } else { + ctx.errorCode(rcode) } + } catch (e: Exception) { + Logs.w(e) + ctx.errnoCode(114514) } - val type = when { - network.endsWith("4") -> DnsResolver.TYPE_A - network.endsWith("6") -> DnsResolver.TYPE_AAAA - else -> null - } - if (type != null) { - DnsResolver.getInstance().query( - SagerNet.underlyingNetwork, - domain, - type, - DnsResolver.FLAG_NO_RETRY, - Dispatchers.IO.asExecutor(), - signal, - callback - ) - } else { - DnsResolver.getInstance().query( - SagerNet.underlyingNetwork, - domain, - DnsResolver.FLAG_NO_RETRY, - Dispatchers.IO.asExecutor(), - signal, - callback - ) + } + + override fun onError(error: DnsResolver.DnsException) { + try { + val cause = error.cause + if (cause is ErrnoException) { + ctx.errnoCode(cause.errno) + } else { + Logs.w(error) + ctx.errnoCode(114514) + } + } catch (e: Exception) { + Logs.w(e) + ctx.errnoCode(114514) } } } + + val type = when { + network.endsWith("4") -> DnsResolver.TYPE_A + network.endsWith("6") -> DnsResolver.TYPE_AAAA + else -> null + } + if (type != null) { + DnsResolver.getInstance().query( + SagerNet.underlyingNetwork, + domain, + type, + DnsResolver.FLAG_NO_RETRY, + Dispatchers.IO.asExecutor(), + signal, + callback + ) + } else { + DnsResolver.getInstance().query( + SagerNet.underlyingNetwork, + domain, + DnsResolver.FLAG_NO_RETRY, + Dispatchers.IO.asExecutor(), + signal, + callback + ) + } } else { - val answer = try { - val u = SagerNet.underlyingNetwork - if (u != null) { - u.getAllByName(domain) - } else { - InetAddress.getAllByName(domain) + runOnIoDispatcher { + // 老版本系统,继续用阻塞的 InetAddress + try { + val u = SagerNet.underlyingNetwork + val answer = try { + u?.getAllByName(domain) + } catch (e: UnknownHostException) { + null + } ?: InetAddress.getAllByName(domain) + if (answer != null) { + ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n")) + } else { + ctx.errnoCode(114514) + } + } catch (e: UnknownHostException) { + ctx.errorCode(RCODE_NXDOMAIN) + } catch (e: Exception) { + Logs.w(e) + ctx.errnoCode(114514) } - } catch (e: UnknownHostException) { - ctx.errorCode(RCODE_NXDOMAIN) - return } - ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n")) } } diff --git a/app/src/main/java/moe/matsuri/nb4a/proxy/config/ConfigBean.java b/app/src/main/java/moe/matsuri/nb4a/proxy/config/ConfigBean.java index b3f18a0f2..df436de76 100644 --- a/app/src/main/java/moe/matsuri/nb4a/proxy/config/ConfigBean.java +++ b/app/src/main/java/moe/matsuri/nb4a/proxy/config/ConfigBean.java @@ -4,6 +4,7 @@ import com.esotericsoftware.kryo.io.ByteBufferInput; import com.esotericsoftware.kryo.io.ByteBufferOutput; +import com.google.gson.JsonObject; import org.jetbrains.annotations.NotNull; @@ -49,7 +50,16 @@ public String displayName() { } public String displayType() { - return type == 0 ? "sing-box config" : "sing-box outbound"; + if (type != null && type == 1 && JavaUtil.isNotBlank(config)) { + try { + JsonObject json = JavaUtil.gson.fromJson(config, JsonObject.class); + if (json != null && json.has("type")) { + return json.get("type").getAsString() + " (sing-box)"; + } + } catch (Exception ignored) { + } + } + return type != null && type == 0 ? "sing-box config" : "sing-box outbound"; } @NotNull diff --git a/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt b/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt index 69a3edb73..da87e9c24 100644 --- a/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt +++ b/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt @@ -20,7 +20,6 @@ import androidx.preference.PreferenceViewHolder import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.ktx.getColorAttr -import io.nekohasekai.sagernet.ktx.isExpertFlavor import kotlin.math.roundToInt class ColorPickerPreference diff --git a/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt b/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt new file mode 100644 index 000000000..31bc7a193 --- /dev/null +++ b/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt @@ -0,0 +1,29 @@ +package moe.matsuri.nb4a.ui + +import android.content.Context +import androidx.core.app.NotificationCompat +import io.nekohasekai.sagernet.R +import io.nekohasekai.sagernet.SagerNet +import io.nekohasekai.sagernet.ktx.Logs + +class ConnectionTestNotification(val context: Context, val title: String) { + private val channelId = "connection-test" + private val notificationId = 1001 + + fun updateNotification(progress: Int, max: Int, finished: Boolean) { + try { + if (finished) { + SagerNet.notification.cancel(notificationId) + return + } + val builder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.ic_service_active) + .setContentTitle(title) + .setOnlyAlertOnce(true) + .setContentText("$progress / $max").setProgress(max, progress, false) + SagerNet.notification.notify(notificationId, builder.build()) + } catch (e: Exception) { + Logs.w(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt b/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt index 6b2ea819a..1292ed5c5 100644 --- a/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt +++ b/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt @@ -4,8 +4,10 @@ import android.content.Context import android.content.Intent import android.util.AttributeSet import androidx.preference.Preference +import io.nekohasekai.sagernet.Key import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.database.DataStore +import io.nekohasekai.sagernet.ktx.Logs import io.nekohasekai.sagernet.ktx.app import io.nekohasekai.sagernet.ui.profile.ConfigEditActivity @@ -26,9 +28,27 @@ class EditConfigPreference : Preference { intent = Intent(context, ConfigEditActivity::class.java) } + var configKey = Key.SERVER_CONFIG + var useConfigStore = false + + fun useConfigStore(key: String) { + try { + this.configKey = key + useConfigStore = true + intent = intent!!.apply { + putExtra("useConfigStore", "1") + putExtra("key", key) + } + } catch (e: Exception) { + Logs.w(e) + } + } + override fun getSummary(): CharSequence { - val config = DataStore.serverConfig - return if (DataStore.serverConfig.isBlank()) { + val config = + (if (useConfigStore) DataStore.configurationStore.getString(configKey) else DataStore.serverConfig) + ?: "" + return if (config.isBlank()) { return app.resources.getString(androidx.preference.R.string.not_set) } else { app.resources.getString(R.string.lines, config.split('\n').size) diff --git a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt index 35df4ee61..c5ae09abb 100644 --- a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt +++ b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt @@ -5,6 +5,8 @@ import android.content.Context import android.util.Base64 import libcore.StringBox import java.io.ByteArrayOutputStream +import java.net.URLDecoder +import java.nio.charset.StandardCharsets import java.text.SimpleDateFormat import java.util.* import java.util.zip.Deflater @@ -132,12 +134,12 @@ object Util { } else if (v is List<*>) { if (k.startsWith("+")) { // prepend val dstKey = k.removePrefix("+") - var currentList = (dst[dstKey] as List<*>).toMutableList() + var currentList = (dst[dstKey] as? List<*>)?.toMutableList() ?: mutableListOf() currentList = (v + currentList).toMutableList() dst[dstKey] = currentList } else if (k.endsWith("+")) { // append val dstKey = k.removeSuffix("+") - var currentList = (dst[dstKey] as List<*>).toMutableList() + var currentList = (dst[dstKey] as? List<*>)?.toMutableList() ?: mutableListOf() currentList = (currentList + v).toMutableList() dst[dstKey] = currentList } else { @@ -150,7 +152,7 @@ object Util { return dst } - fun mergeJSON(j: String, dst: MutableMap) { + fun mergeJSON(dst: MutableMap, j: String) { if (j.isBlank()) return val src = JavaUtil.gson.fromJson(j, dst.javaClass) mergeMap(dst, src) @@ -188,4 +190,11 @@ object Util { } return "" } + + fun decodeFilename(headerValue: String): String { + val regex = Regex("filename\\*=[^']*''(.+)") + val match = regex.find(headerValue) + val encoded = match?.groupValues?.get(1) ?: "" + return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name()) + } } diff --git a/app/src/main/res/layout/layout_app_list.xml b/app/src/main/res/layout/layout_app_list.xml index 45432fa45..4128a887f 100644 --- a/app/src/main/res/layout/layout_app_list.xml +++ b/app/src/main/res/layout/layout_app_list.xml @@ -116,4 +116,8 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:listitem="@layout/layout_apps_item" /> + + diff --git a/app/src/main/res/layout/layout_app_placeholder.xml b/app/src/main/res/layout/layout_app_placeholder.xml new file mode 100644 index 000000000..456ac8a2b --- /dev/null +++ b/app/src/main/res/layout/layout_app_placeholder.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/main/res/layout/layout_apps.xml b/app/src/main/res/layout/layout_apps.xml index 1dd32b265..e648248a2 100644 --- a/app/src/main/res/layout/layout_apps.xml +++ b/app/src/main/res/layout/layout_apps.xml @@ -153,4 +153,8 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:listitem="@layout/layout_apps_item" /> + + diff --git a/app/src/main/res/layout/layout_backup.xml b/app/src/main/res/layout/layout_backup.xml index fb61aaf98..81a7decee 100644 --- a/app/src/main/res/layout/layout_backup.xml +++ b/app/src/main/res/layout/layout_backup.xml @@ -5,6 +5,14 @@ android:orientation="vertical" android:padding="16dp"> +