diff --git a/app/src/androidTest/java/com/futo/platformplayer/GEncryptionProviderTests.kt b/app/src/androidTest/java/com/futo/platformplayer/GEncryptionProviderTests.kt new file mode 100644 index 0000000000000000000000000000000000000000..66dde080bf797cb81d2804b69717658e1508fa61 --- /dev/null +++ b/app/src/androidTest/java/com/futo/platformplayer/GEncryptionProviderTests.kt @@ -0,0 +1,78 @@ +package com.futo.platformplayer + +import com.futo.platformplayer.encryption.GEncryptionProviderV0 +import com.futo.platformplayer.encryption.GEncryptionProviderV1 +import junit.framework.TestCase.assertEquals +import org.junit.Test + +class GEncryptionProviderTests { + @Test + fun testEncryptDecryptV1() { + val encryptionProvider = GEncryptionProviderV1.instance + val plaintext = "This is a test string." + + // Encrypt the plaintext + val ciphertext = encryptionProvider.encrypt(plaintext) + + // Decrypt the ciphertext + val decrypted = encryptionProvider.decrypt(ciphertext) + + // The decrypted string should be equal to the original plaintext + assertEquals(plaintext, decrypted) + } + + + @Test + fun testEncryptDecryptBytesV1() { + val encryptionProvider = GEncryptionProviderV1.instance + val bytes = "This is a test string.".toByteArray(); + + // Encrypt the plaintext + val ciphertext = encryptionProvider.encrypt(bytes) + + // Decrypt the ciphertext + val decrypted = encryptionProvider.decrypt(ciphertext) + + // The decrypted string should be equal to the original plaintext + assertArrayEquals(bytes, decrypted); + } + + + @Test + fun testEncryptDecryptV0() { + val encryptionProvider = GEncryptionProviderV0.instance + val plaintext = "This is a test string." + + // Encrypt the plaintext + val ciphertext = encryptionProvider.encrypt(plaintext) + + // Decrypt the ciphertext + val decrypted = encryptionProvider.decrypt(ciphertext) + + // The decrypted string should be equal to the original plaintext + assertEquals(plaintext, decrypted) + } + + + @Test + fun testEncryptDecryptBytesV0() { + val encryptionProvider = GEncryptionProviderV0.instance + val bytes = "This is a test string.".toByteArray(); + + // Encrypt the plaintext + val ciphertext = encryptionProvider.encrypt(bytes) + + // Decrypt the ciphertext + val decrypted = encryptionProvider.decrypt(ciphertext) + + // The decrypted string should be equal to the original plaintext + assertArrayEquals(bytes, decrypted); + } + + private fun assertArrayEquals(a: ByteArray, b: ByteArray) { + assertEquals(a.size, b.size); + for(i in 0 until a.size) { + assertEquals(a[i], b[i]); + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/futo/platformplayer/EncryptionProviderTests.kt b/app/src/androidTest/java/com/futo/platformplayer/GPasswordEncryptionProviderTests.kt similarity index 55% rename from app/src/androidTest/java/com/futo/platformplayer/EncryptionProviderTests.kt rename to app/src/androidTest/java/com/futo/platformplayer/GPasswordEncryptionProviderTests.kt index 51319782c8fee65ef8661ac29c7afdcbdb3c2904..498a964932bc63a4ce86e4cc7728a4d3d4e61f9f 100644 --- a/app/src/androidTest/java/com/futo/platformplayer/EncryptionProviderTests.kt +++ b/app/src/androidTest/java/com/futo/platformplayer/GPasswordEncryptionProviderTests.kt @@ -1,29 +1,29 @@ package com.futo.platformplayer -import com.futo.platformplayer.encryption.EncryptionProvider +import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0 +import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV1 import junit.framework.TestCase.assertEquals import org.junit.Test -class EncryptionProviderTests { +class GPasswordEncryptionProviderTests { @Test - fun testEncryptDecrypt() { - val encryptionProvider = EncryptionProvider.instance - val plaintext = "This is a test string." + fun testEncryptDecryptBytesPasswordV1() { + val encryptionProvider = GPasswordEncryptionProviderV1(); + val bytes = "This is a test string.".toByteArray(); // Encrypt the plaintext - val ciphertext = encryptionProvider.encrypt(plaintext) + val ciphertext = encryptionProvider.encrypt(bytes, "1234") // Decrypt the ciphertext - val decrypted = encryptionProvider.decrypt(ciphertext) + val decrypted = encryptionProvider.decrypt(ciphertext, "1234") // The decrypted string should be equal to the original plaintext - assertEquals(plaintext, decrypted) + assertArrayEquals(bytes, decrypted); } - @Test - fun testEncryptDecryptBytes() { - val encryptionProvider = EncryptionProvider.instance + fun testEncryptDecryptBytesPasswordV0() { + val encryptionProvider = GPasswordEncryptionProviderV0("1234".padStart(32, '9')); val bytes = "This is a test string.".toByteArray(); // Encrypt the plaintext diff --git a/app/src/androidTest/java/com/futo/platformplayer/PasswordEncryptionProviderTests.kt b/app/src/androidTest/java/com/futo/platformplayer/PasswordEncryptionProviderTests.kt deleted file mode 100644 index 98d26a222acc4300cd722f4f698a2eed3cebe961..0000000000000000000000000000000000000000 --- a/app/src/androidTest/java/com/futo/platformplayer/PasswordEncryptionProviderTests.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.futo.platformplayer - -import com.futo.platformplayer.encryption.EncryptionProvider -import com.futo.platformplayer.encryption.PasswordEncryptionProvider -import junit.framework.TestCase.assertEquals -import org.junit.Test - -class PasswordEncryptionProviderTests { - @Test - fun testEncryptDecryptBytesPassword() { - val password = "1234".padStart(32, '9'); - val encryptionProvider = PasswordEncryptionProvider(password); - val bytes = "This is a test string.".toByteArray(); - - // Encrypt the plaintext - val ciphertext = encryptionProvider.encrypt(bytes) - - // Decrypt the ciphertext - val decrypted = encryptionProvider.decrypt(ciphertext) - - // The decrypted string should be equal to the original plaintext - assertArrayEquals(bytes, decrypted); - } - - private fun assertArrayEquals(a: ByteArray, b: ByteArray) { - assertEquals(a.size, b.size); - for(i in 0 until a.size) { - assertEquals(a[i], b[i]); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt index db3cd0b9d6aeb3283f9c856effcb4176fe5bfbbb..83a9840f696394301d0e476483aa9b150bde6edb 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt @@ -29,6 +29,7 @@ import com.futo.polycentric.core.Store import com.futo.polycentric.core.Synchronization import com.futo.polycentric.core.SystemState import com.futo.polycentric.core.toURLInfoDataLink +import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.github.dhaval2404.imagepicker.ImagePicker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -222,7 +223,7 @@ class PolycentricProfileActivity : AppCompatActivity() { val avatar = systemState.avatar.selectBestImage(dp_80 * dp_80); Glide.with(_imagePolycentric) - .load(avatar?.toURLInfoDataLink(processHandle.system.toProto(), processHandle.processSecret.process.toProto(), systemState.servers.toList())) + .load(avatar?.toURLInfoSystemLinkUrl(processHandle.system.toProto(), avatar.process, systemState.servers.toList())) .placeholder(R.drawable.placeholder_profile) .crossfade() .into(_imagePolycentric) diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceAuth.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceAuth.kt index 0bf0d96d6390e02cd554f4ae4c7c615e19824e25..d2c705b8d8d323be06085c9e76f6ddcf2b583462 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceAuth.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceAuth.kt @@ -1,20 +1,17 @@ package com.futo.platformplayer.api.media.platforms.js -import com.futo.platformplayer.encryption.EncryptionProvider -import com.futo.platformplayer.logging.Logger import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json - data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) { override fun toString(): String { return "(headers: '$headers', cookieString: '$cookieMap')"; } fun toEncrypted(): String{ - return EncryptionProvider.instance.encrypt(serialize()); + return SourceEncrypted.fromDecrypted { serialize() }.toJson(); } private fun serialize(): String { @@ -25,20 +22,10 @@ data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? = val TAG = "SourceAuth"; fun fromEncrypted(encrypted: String?): SourceAuth? { - if(encrypted == null) - return null; - - val decrypted = EncryptionProvider.instance.decrypt(encrypted); - try { - return deserialize(decrypted); - } - catch(ex: Throwable) { - Logger.e(TAG, "Failed to deserialize authentication", ex); - return null; - } + return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) }; } - fun deserialize(str: String): SourceAuth { + private fun deserialize(str: String): SourceAuth { val data = Json.decodeFromString<SerializedAuth>(str); return SourceAuth(data.cookieMap, data.headers); } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceCaptchaData.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceCaptchaData.kt index 7dceeef2a1637919641a5c9355f8440ac5e500ad..140a19d0727a14e30527f482c7565bf0b85273cf 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceCaptchaData.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceCaptchaData.kt @@ -1,7 +1,5 @@ package com.futo.platformplayer.api.media.platforms.js -import com.futo.platformplayer.encryption.EncryptionProvider -import com.futo.platformplayer.logging.Logger import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString @@ -13,7 +11,7 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri } fun toEncrypted(): String{ - return EncryptionProvider.instance.encrypt(serialize()); + return SourceEncrypted.fromDecrypted { serialize() }.toJson(); } private fun serialize(): String { @@ -21,20 +19,10 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri } companion object { - val TAG = "SourceAuth"; + val TAG = "SourceCaptchaData"; fun fromEncrypted(encrypted: String?): SourceCaptchaData? { - if(encrypted == null) - return null; - - val decrypted = EncryptionProvider.instance.decrypt(encrypted); - try { - return deserialize(decrypted); - } - catch(ex: Throwable) { - Logger.e(TAG, "Failed to deserialize authentication", ex); - return null; - } + return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) }; } fun deserialize(str: String): SourceCaptchaData { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceEncrypted.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceEncrypted.kt new file mode 100644 index 0000000000000000000000000000000000000000..27868a30e701f78ee380fa9999cf36ee419f261d --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourceEncrypted.kt @@ -0,0 +1,59 @@ +package com.futo.platformplayer.api.media.platforms.js + +import com.futo.platformplayer.encryption.GEncryptionProvider +import com.futo.platformplayer.encryption.GEncryptionProviderV0 +import com.futo.platformplayer.logging.Logger +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.lang.Exception + +@Serializable +data class SourceEncrypted( + val encrypted: String, + val version: Int = GEncryptionProvider.version +) { + fun toJson(): String { + return Json.encodeToString(this); + } + + companion object { + fun fromDecrypted(serializer: () -> String): SourceEncrypted { + return SourceEncrypted(GEncryptionProvider.instance.encrypt(serializer())); + } + + fun <T> decryptEncrypted(encrypted: String?, deserializer: (decrypted: String) -> T): T? { + if(encrypted == null) + return null; + + try { + val encryptedSourceAuth = Json.decodeFromString<SourceEncrypted>(encrypted) + if (encryptedSourceAuth.version != GEncryptionProvider.version) { + throw Exception("Invalid encryption version."); + } + + val decrypted = GEncryptionProvider.instance.decrypt(encryptedSourceAuth.encrypted); + try { + return deserializer(decrypted); + } catch(ex: Throwable) { + Logger.e(SourceAuth.TAG, "Failed to deserialize SourceEncrypted<T>", ex); + return null; + } + } catch (e: Throwable) { + //Try to fall back to old mechanism, remove this eventually + if (!encrypted.contains("version")) { + val decrypted = GEncryptionProviderV0.instance.decrypt(encrypted); + try { + return deserializer(decrypted); + } catch (ex: Throwable) { + Logger.e(SourceAuth.TAG, "Failed to deserialize SourceEncrypted<T>", ex); + return null; + } + } else { + return null; + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/encryption/GEncryptionProvider.kt b/app/src/main/java/com/futo/platformplayer/encryption/GEncryptionProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8ed867c2501669f88ec85e824031ec48dcf388c --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/encryption/GEncryptionProvider.kt @@ -0,0 +1,8 @@ +package com.futo.platformplayer.encryption + +class GEncryptionProvider { + companion object { + val instance: GEncryptionProviderV1 = GEncryptionProviderV1.instance; + val version = 1; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/encryption/EncryptionProvider.kt b/app/src/main/java/com/futo/platformplayer/encryption/GEncryptionProviderV0.kt similarity index 89% rename from app/src/main/java/com/futo/platformplayer/encryption/EncryptionProvider.kt rename to app/src/main/java/com/futo/platformplayer/encryption/GEncryptionProviderV0.kt index d02b54f2e639cda8326f169aa63f6a6ee37d3d9d..8f75a2f55cccf7d4a2f99ffc8e061791b2814b51 100644 --- a/app/src/main/java/com/futo/platformplayer/encryption/EncryptionProvider.kt +++ b/app/src/main/java/com/futo/platformplayer/encryption/GEncryptionProviderV0.kt @@ -3,15 +3,13 @@ package com.futo.platformplayer.encryption import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 -import com.futo.polycentric.core.EncryptionProvider import java.security.Key import java.security.KeyStore import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.spec.GCMParameterSpec -import javax.crypto.spec.SecretKeySpec -class EncryptionProvider { +class GEncryptionProviderV0 { private val _keyStore: KeyStore; private val secretKey: Key? get() = _keyStore.getKey(KEY_ALIAS, null); @@ -38,30 +36,31 @@ class EncryptionProvider { } fun encrypt(decrypted: ByteArray): ByteArray { val c: Cipher = Cipher.getInstance(AES_MODE); - c.init(Cipher.ENCRYPT_MODE, secretKey, GCMParameterSpec(128, FIXED_IV)); + c.init(Cipher.ENCRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV)); val encodedBytes: ByteArray = c.doFinal(decrypted); return encodedBytes; } fun decrypt(encrypted: String): String { val c = Cipher.getInstance(AES_MODE); - c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, FIXED_IV)); + c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV)); val decrypted = String(c.doFinal(Base64.decode(encrypted, Base64.DEFAULT))); return decrypted; } fun decrypt(encrypted: ByteArray): ByteArray { val c = Cipher.getInstance(AES_MODE); - c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, FIXED_IV)); + c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV)); return c.doFinal(encrypted); } companion object { - val instance: EncryptionProvider = EncryptionProvider(); + val instance: GEncryptionProviderV0 = GEncryptionProviderV0(); private val FIXED_IV = byteArrayOf(12, 43, 127, 2, 99, 22, 6, 78, 24, 53, 8, 101); private const val AndroidKeyStore = "AndroidKeyStore"; private const val KEY_ALIAS = "FUTOMedia_Key"; private const val AES_MODE = "AES/GCM/NoPadding"; - private val TAG = "EncryptionProvider"; + private const val TAG_LENGTH = 128 + private val TAG = "GEncryptionProviderV0"; } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/encryption/GEncryptionProviderV1.kt b/app/src/main/java/com/futo/platformplayer/encryption/GEncryptionProviderV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..06e562068e8816f11fa1999371a1a493a3f5626e --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/encryption/GEncryptionProviderV1.kt @@ -0,0 +1,76 @@ +package com.futo.platformplayer.encryption + +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import java.security.Key +import java.security.KeyStore +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.spec.GCMParameterSpec + +class GEncryptionProviderV1 { + private val _keyStore: KeyStore; + private val secretKey: Key? get() = _keyStore.getKey(KEY_ALIAS, null); + + constructor() { + _keyStore = KeyStore.getInstance(AndroidKeyStore); + _keyStore.load(null); + + if (!_keyStore.containsAlias(KEY_ALIAS)) { + val keyGenerator: KeyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore) + keyGenerator.init(KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setRandomizedEncryptionRequired(false) + .build()); + + keyGenerator.generateKey(); + } + } + + fun encrypt(decrypted: String): String { + val encrypted = encrypt(decrypted.toByteArray()); + val encoded = Base64.encodeToString(encrypted, Base64.DEFAULT); + return encoded; + } + fun encrypt(decrypted: ByteArray): ByteArray { + val ivBytes = generateIv() + val c: Cipher = Cipher.getInstance(AES_MODE); + c.init(Cipher.ENCRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, ivBytes)); + val encodedBytes: ByteArray = c.doFinal(decrypted); + return ivBytes + encodedBytes; + } + + fun decrypt(data: String): String { + val bytes = Base64.decode(data, Base64.DEFAULT) + return String(decrypt(bytes)); + } + fun decrypt(bytes: ByteArray): ByteArray { + val encrypted = bytes.sliceArray(IntRange(IV_SIZE, bytes.size - 1)) + val ivBytes = bytes.sliceArray(IntRange(0, IV_SIZE - 1)) + + val c = Cipher.getInstance(AES_MODE); + c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, ivBytes)); + return c.doFinal(encrypted); + } + + private fun generateIv(): ByteArray { + val r = SecureRandom() + val ivBytes = ByteArray(IV_SIZE) + r.nextBytes(ivBytes) + return ivBytes + } + + companion object { + val instance: GEncryptionProviderV1 = GEncryptionProviderV1(); + + private const val AndroidKeyStore = "AndroidKeyStore"; + private const val KEY_ALIAS = "FUTOMedia_Key"; + private const val AES_MODE = "AES/GCM/NoPadding"; + private const val IV_SIZE = 12; + private const val TAG_LENGTH = 128 + private val TAG = "GEncryptionProviderV1"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/encryption/GPasswordEncryptionProvider.kt b/app/src/main/java/com/futo/platformplayer/encryption/GPasswordEncryptionProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..83621f65936e8fbe63504a5a264970b73588ad23 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/encryption/GPasswordEncryptionProvider.kt @@ -0,0 +1,8 @@ +package com.futo.platformplayer.encryption + +class GPasswordEncryptionProvider { + companion object { + val version = 1; + val instance = GPasswordEncryptionProviderV1.instance; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/encryption/PasswordEncryptionProvider.kt b/app/src/main/java/com/futo/platformplayer/encryption/GPasswordEncryptionProviderV0.kt similarity index 76% rename from app/src/main/java/com/futo/platformplayer/encryption/PasswordEncryptionProvider.kt rename to app/src/main/java/com/futo/platformplayer/encryption/GPasswordEncryptionProviderV0.kt index 3b694b1935b6f3cf9346531fcd73451669f7952e..4705bab964a2748a441de4502d5d2206a7cb6660 100644 --- a/app/src/main/java/com/futo/platformplayer/encryption/PasswordEncryptionProvider.kt +++ b/app/src/main/java/com/futo/platformplayer/encryption/GPasswordEncryptionProviderV0.kt @@ -1,4 +1,3 @@ - package com.futo.platformplayer.encryption import android.util.Base64 @@ -6,7 +5,7 @@ import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec -class PasswordEncryptionProvider { +class GPasswordEncryptionProviderV0 { private val _key: SecretKeySpec; constructor(password: String) { @@ -20,26 +19,27 @@ class PasswordEncryptionProvider { } fun encrypt(decrypted: ByteArray): ByteArray { val c: Cipher = Cipher.getInstance(AES_MODE); - c.init(Cipher.ENCRYPT_MODE, _key, GCMParameterSpec(128, FIXED_IV)); + c.init(Cipher.ENCRYPT_MODE, _key, GCMParameterSpec(TAG_LENGTH, FIXED_IV)); val encodedBytes: ByteArray = c.doFinal(decrypted); return encodedBytes; } fun decrypt(encrypted: String): String { val c = Cipher.getInstance(AES_MODE); - c.init(Cipher.DECRYPT_MODE, _key, GCMParameterSpec(128, FIXED_IV)); + c.init(Cipher.DECRYPT_MODE, _key, GCMParameterSpec(TAG_LENGTH, FIXED_IV)); val decrypted = String(c.doFinal(Base64.decode(encrypted, Base64.DEFAULT))); return decrypted; } fun decrypt(encrypted: ByteArray): ByteArray { val c = Cipher.getInstance(AES_MODE); - c.init(Cipher.DECRYPT_MODE, _key, GCMParameterSpec(128, FIXED_IV)); + c.init(Cipher.DECRYPT_MODE, _key, GCMParameterSpec(TAG_LENGTH, FIXED_IV)); return c.doFinal(encrypted); } companion object { private val FIXED_IV = byteArrayOf(12, 43, 127, 2, 99, 22, 6, 78, 24, 53, 8, 101); + private const val TAG_LENGTH = 128 private const val AES_MODE = "AES/GCM/NoPadding"; - private val TAG = "PasswordEncryptionProvider"; + private val TAG = "GPasswordEncryptionProviderV0"; } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/encryption/GPasswordEncryptionProviderV1.kt b/app/src/main/java/com/futo/platformplayer/encryption/GPasswordEncryptionProviderV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..bd7aa49d842c202836b54facf9f7012d389dd365 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/encryption/GPasswordEncryptionProviderV1.kt @@ -0,0 +1,75 @@ +package com.futo.platformplayer.encryption + +import android.util.Base64 +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec + +class GPasswordEncryptionProviderV1 { + fun encrypt(decrypted: String, password: String): String { + val encrypted = encrypt(decrypted.toByteArray(), password); + val encoded = Base64.encodeToString(encrypted, Base64.DEFAULT); + return encoded; + } + + fun encrypt(decrypted: ByteArray, password: String): ByteArray { + val saltBytes = generateSalt() + val ivBytes = generateIv() + val c: Cipher = Cipher.getInstance(AES_MODE); + val key = deriveKeyFromPassword(password, saltBytes) + + c.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(TAG_LENGTH, ivBytes)); + val encodedBytes: ByteArray = c.doFinal(decrypted); + return saltBytes + ivBytes + encodedBytes; + } + + fun decrypt(data: String, password: String): String { + val bytes = Base64.decode(data, Base64.DEFAULT) + return String(decrypt(bytes, password)); + } + fun decrypt(bytes: ByteArray, password: String): ByteArray { + val encrypted = bytes.sliceArray(IntRange(SALT_SIZE + IV_SIZE, bytes.size - 1)) + val ivBytes = bytes.sliceArray(IntRange(SALT_SIZE, SALT_SIZE + IV_SIZE - 1)) + val saltBytes = bytes.sliceArray(IntRange(0, SALT_SIZE - 1)) + val key = deriveKeyFromPassword(password, saltBytes) + + val c = Cipher.getInstance(AES_MODE); + c.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(TAG_LENGTH, ivBytes)); + return c.doFinal(encrypted); + } + + private fun deriveKeyFromPassword(password: String, salt: ByteArray): SecretKeySpec { + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val spec = PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH) + val tmp = factory.generateSecret(spec) + return SecretKeySpec(tmp.encoded, "AES") + } + + private fun generateSalt(): ByteArray { + val random = SecureRandom() + val salt = ByteArray(SALT_SIZE) + random.nextBytes(salt) + return salt + } + + private fun generateIv(): ByteArray { + val r = SecureRandom() + val ivBytes = ByteArray(IV_SIZE) + r.nextBytes(ivBytes) + return ivBytes + } + + companion object { + val instance = GPasswordEncryptionProviderV1(); + private const val AES_MODE = "AES/GCM/NoPadding"; + private const val IV_SIZE = 12 + private const val SALT_SIZE = 16 + private const val ITERATION_COUNT = 2 * 65536 + private const val KEY_LENGTH = 256 + private const val TAG_LENGTH = 128 + private val TAG = "GPasswordEncryptionProviderV1"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ImportSubscriptionsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ImportSubscriptionsFragment.kt index db2fa74a031878202129415d3213e31f09a36370..2f3c93f613eeb9de31ca6710b277a806a153e4cb 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ImportSubscriptionsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ImportSubscriptionsFragment.kt @@ -63,6 +63,7 @@ class ImportSubscriptionsFragment : MainFragment() { private var _textSelectDeselectAll: TextView; private var _textNothingToImport: TextView; private var _textCounter: TextView; + private var _textLoadMore: TextView; private var _adapterView: AnyAdapterView<SelectableIPlatformChannel, ImportSubscriptionViewHolder>; private var _links: List<String> = listOf(); private val _items: ArrayList<SelectableIPlatformChannel> = arrayListOf(); @@ -79,6 +80,7 @@ class ImportSubscriptionsFragment : MainFragment() { _textNothingToImport = findViewById(R.id.nothing_to_import); _textSelectDeselectAll = findViewById(R.id.text_select_deselect_all); _textCounter = findViewById(R.id.text_select_counter); + _textLoadMore = findViewById(R.id.text_load_more); _spinner = findViewById(R.id.channel_loader); _adapterView = findViewById<RecyclerView>(R.id.recycler_import).asAny( _items) { @@ -120,6 +122,19 @@ class ImportSubscriptionsFragment : MainFragment() { //UIDialogs.showDataRetryDialog(layoutInflater, { load(); }); loadNext(); }; + + _textLoadMore.setOnClickListener { + if (!_limitToastShown) { + return@setOnClickListener; + } + + _textLoadMore.visibility = View.GONE; + _limitToastShown = false; + _counter = 0; + load(); + }; + + _textLoadMore.visibility = View.GONE; } fun cleanup() { @@ -165,7 +180,8 @@ class ImportSubscriptionsFragment : MainFragment() { if (_counter >= MAXIMUM_BATCH_SIZE) { if (!_limitToastShown) { _limitToastShown = true; - UIDialogs.toast(context, "Stopped after {requestCount} to avoid rate limit, re-enter to import rest".replace("{requestCount}", MAXIMUM_BATCH_SIZE.toString())); + _textLoadMore.visibility = View.VISIBLE; + UIDialogs.toast(context, context.getString(R.string.stopped_after_requestcount_to_avoid_rate_limit_click_load_more_to_load_more).replace("{requestCount}", MAXIMUM_BATCH_SIZE.toString())); } setLoading(false); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt b/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt index 9667f7ac44c862231df2735caa3509c5cf49b460..69fbb5fef8897bd2c395b0c1c9592c380cde2106 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateBackup.kt @@ -1,29 +1,19 @@ package com.futo.platformplayer.states -import android.app.Activity import android.content.Context -import android.content.Intent -import android.net.Uri -import android.provider.DocumentsContract.EXTRA_INITIAL_URI -import androidx.activity.ComponentActivity import androidx.core.app.ShareCompat import androidx.core.content.FileProvider -import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.activities.IWithResultLauncher -import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.copyTo -import com.futo.platformplayer.copyToOutputStream -import com.futo.platformplayer.encryption.EncryptionProvider -import com.futo.platformplayer.encryption.PasswordEncryptionProvider -import com.futo.platformplayer.getInputStream +import com.futo.platformplayer.encryption.GPasswordEncryptionProvider +import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0 import com.futo.platformplayer.getNowDiffHours -import com.futo.platformplayer.getOutputStream import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.readBytes import com.futo.platformplayer.stores.FragmentedStorage @@ -39,9 +29,8 @@ import kotlinx.serialization.json.Json import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File -import java.io.FileInputStream import java.io.FileNotFoundException -import java.io.InputStream +import java.lang.Exception import java.time.OffsetDateTime import java.util.zip.ZipEntry import java.util.zip.ZipInputStream @@ -83,7 +72,7 @@ class StateBackup { val pbytes = password.toByteArray(); if(pbytes.size < 4 || pbytes.size > 32) throw IllegalStateException("Automatic backup passwords should atleast be 4 character and smaller than 32"); - return password.padStart(32, '9'); + return password; } fun hasAutomaticBackup(): Boolean { val context = StateApp.instance.contextOrNull ?: return false; @@ -107,8 +96,8 @@ class StateBackup { val data = export(); val zip = data.asZip(); - val encryptedZip = PasswordEncryptionProvider(getAutomaticBackupPassword()).encrypt(zip); - + //Prepend some magic bytes to identify everything version 1 and up + val encryptedZip = byteArrayOf(0x11, 0x22, 0x33, 0x44, GPasswordEncryptionProvider.version.toByte()) + GPasswordEncryptionProvider.instance.encrypt(zip, getAutomaticBackupPassword()); if(!Settings.instance.storage.isStorageMainValid(context)) { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { UIDialogs.toast("Missing permissions for auto-backup, please set the external general directory in settings"); @@ -152,8 +141,7 @@ class StateBackup { throw IllegalStateException("Backup file does not exist"); val backupBytesEncrypted = backupFiles.first!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.first?.uri}]"); - val backupBytes = PasswordEncryptionProvider(getAutomaticBackupPassword(password)).decrypt(backupBytesEncrypted); - importZipBytes(context, scope, backupBytes); + importEncryptedZipBytes(context, scope, backupBytesEncrypted, password); Logger.i(TAG, "Finished AutoBackup restore"); } catch (exSec: FileNotFoundException) { @@ -180,13 +168,30 @@ class StateBackup { throw ex; val backupBytesEncrypted = backupFiles.second!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.second?.uri}]"); - val backupBytes = PasswordEncryptionProvider(getAutomaticBackupPassword(password)).decrypt(backupBytesEncrypted); - importZipBytes(context, scope, backupBytes); + importEncryptedZipBytes(context, scope, backupBytesEncrypted, password); Logger.i(TAG, "Finished AutoBackup restore"); } } } + private fun importEncryptedZipBytes(context: Context, scope: CoroutineScope, backupBytesEncrypted: ByteArray, password: String) { + val backupBytes: ByteArray; + //Check magic bytes indicating version 1 and up + if (backupBytesEncrypted[0] == 0x11.toByte() && backupBytesEncrypted[1] == 0x22.toByte() && backupBytesEncrypted[2] == 0x33.toByte() && backupBytesEncrypted[3] == 0x44.toByte()) { + val version = backupBytesEncrypted[4].toInt(); + if (version != GPasswordEncryptionProvider.version) { + throw Exception("Invalid encryption version"); + } + + backupBytes = GPasswordEncryptionProvider.instance.decrypt(backupBytesEncrypted.sliceArray(IntRange(5, backupBytesEncrypted.size - 1)), getAutomaticBackupPassword(password)) + } else { + //Else its a version 0 + backupBytes = GPasswordEncryptionProviderV0(getAutomaticBackupPassword(password).padStart(32, '9')).decrypt(backupBytesEncrypted); + } + + importZipBytes(context, scope, backupBytes); + } + fun startExternalBackup() { val data = export(); val now = OffsetDateTime.now(); diff --git a/app/src/main/res/layout/fragment_import.xml b/app/src/main/res/layout/fragment_import.xml index 13b9fdf2761833c319955c915103979ddf8683b4..1df4194911e58961620e18555325fbc008496800 100644 --- a/app/src/main/res/layout/fragment_import.xml +++ b/app/src/main/res/layout/fragment_import.xml @@ -40,6 +40,19 @@ android:layout_height="match_parent" android:layout_weight="1" /> + <TextView + android:id="@+id/text_load_more" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@font/inter_light" + android:textSize="15dp" + android:text="@string/load_more" + android:textColor="@color/colorPrimary" /> + + <Space android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" /> + <TextView android:id="@+id/text_select_counter" android:layout_width="wrap_content" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 05a86c00a8b0e9fb923c81dcd16957dd861e0319..477f317cc2da6c0bb60acb48e30b1bf2a161286b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -637,6 +637,8 @@ <string name="more_options">More Options</string> <string name="save">Save</string> <string name="this_creator_has_not_set_any_support_options_on_harbor_polycentric">This creator has not set any support options on Harbor (Polycentric)</string> + <string name="load_more">Load More</string> + <string name="stopped_after_requestcount_to_avoid_rate_limit_click_load_more_to_load_more">Stopped after {requestCount} to avoid rate limit, click load more to load more.</string> <string-array name="home_screen_array"> <item>Recommendations</item> <item>Subscriptions</item> diff --git a/dep/polycentricandroid b/dep/polycentricandroid index 3fe81ffb3c730f0dc9f645149dc20c31284cd790..1b041a8612b98bd0fb7cb0fbb672933674a69035 160000 --- a/dep/polycentricandroid +++ b/dep/polycentricandroid @@ -1 +1 @@ -Subproject commit 3fe81ffb3c730f0dc9f645149dc20c31284cd790 +Subproject commit 1b041a8612b98bd0fb7cb0fbb672933674a69035