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