Encryption changes, load more on import subscriptions.

This commit is contained in:
Koen 2023-11-09 15:04:54 +00:00
parent 00e40e8cd6
commit 0de996d91c
18 changed files with 395 additions and 111 deletions

View File

@ -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]);
}
}
}

View File

@ -1,29 +1,29 @@
package com.futo.platformplayer 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 junit.framework.TestCase.assertEquals
import org.junit.Test import org.junit.Test
class EncryptionProviderTests { class GPasswordEncryptionProviderTests {
@Test @Test
fun testEncryptDecrypt() { fun testEncryptDecryptBytesPasswordV1() {
val encryptionProvider = EncryptionProvider.instance val encryptionProvider = GPasswordEncryptionProviderV1();
val plaintext = "This is a test string." val bytes = "This is a test string.".toByteArray();
// Encrypt the plaintext // Encrypt the plaintext
val ciphertext = encryptionProvider.encrypt(plaintext) val ciphertext = encryptionProvider.encrypt(bytes, "1234")
// Decrypt the ciphertext // Decrypt the ciphertext
val decrypted = encryptionProvider.decrypt(ciphertext) val decrypted = encryptionProvider.decrypt(ciphertext, "1234")
// The decrypted string should be equal to the original plaintext // The decrypted string should be equal to the original plaintext
assertEquals(plaintext, decrypted) assertArrayEquals(bytes, decrypted);
} }
@Test @Test
fun testEncryptDecryptBytes() { fun testEncryptDecryptBytesPasswordV0() {
val encryptionProvider = EncryptionProvider.instance val encryptionProvider = GPasswordEncryptionProviderV0("1234".padStart(32, '9'));
val bytes = "This is a test string.".toByteArray(); val bytes = "This is a test string.".toByteArray();
// Encrypt the plaintext // Encrypt the plaintext

View File

@ -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]);
}
}
}

View File

@ -29,6 +29,7 @@ import com.futo.polycentric.core.Store
import com.futo.polycentric.core.Synchronization import com.futo.polycentric.core.Synchronization
import com.futo.polycentric.core.SystemState import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.toURLInfoDataLink import com.futo.polycentric.core.toURLInfoDataLink
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePicker
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -222,7 +223,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
val avatar = systemState.avatar.selectBestImage(dp_80 * dp_80); val avatar = systemState.avatar.selectBestImage(dp_80 * dp_80);
Glide.with(_imagePolycentric) 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) .placeholder(R.drawable.placeholder_profile)
.crossfade() .crossfade()
.into(_imagePolycentric) .into(_imagePolycentric)

View File

@ -1,20 +1,17 @@
package com.futo.platformplayer.api.media.platforms.js 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.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) { data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) {
override fun toString(): String { override fun toString(): String {
return "(headers: '$headers', cookieString: '$cookieMap')"; return "(headers: '$headers', cookieString: '$cookieMap')";
} }
fun toEncrypted(): String{ fun toEncrypted(): String{
return EncryptionProvider.instance.encrypt(serialize()); return SourceEncrypted.fromDecrypted { serialize() }.toJson();
} }
private fun serialize(): String { private fun serialize(): String {
@ -25,20 +22,10 @@ data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? =
val TAG = "SourceAuth"; val TAG = "SourceAuth";
fun fromEncrypted(encrypted: String?): SourceAuth? { fun fromEncrypted(encrypted: String?): SourceAuth? {
if(encrypted == null) return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) };
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;
}
} }
fun deserialize(str: String): SourceAuth { private fun deserialize(str: String): SourceAuth {
val data = Json.decodeFromString<SerializedAuth>(str); val data = Json.decodeFromString<SerializedAuth>(str);
return SourceAuth(data.cookieMap, data.headers); return SourceAuth(data.cookieMap, data.headers);
} }

View File

@ -1,7 +1,5 @@
package com.futo.platformplayer.api.media.platforms.js 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.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@ -13,7 +11,7 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri
} }
fun toEncrypted(): String{ fun toEncrypted(): String{
return EncryptionProvider.instance.encrypt(serialize()); return SourceEncrypted.fromDecrypted { serialize() }.toJson();
} }
private fun serialize(): String { private fun serialize(): String {
@ -21,20 +19,10 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri
} }
companion object { companion object {
val TAG = "SourceAuth"; val TAG = "SourceCaptchaData";
fun fromEncrypted(encrypted: String?): SourceCaptchaData? { fun fromEncrypted(encrypted: String?): SourceCaptchaData? {
if(encrypted == null) return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) };
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;
}
} }
fun deserialize(str: String): SourceCaptchaData { fun deserialize(str: String): SourceCaptchaData {

View File

@ -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;
}
}
}
}
}

View File

@ -0,0 +1,8 @@
package com.futo.platformplayer.encryption
class GEncryptionProvider {
companion object {
val instance: GEncryptionProviderV1 = GEncryptionProviderV1.instance;
val version = 1;
}
}

View File

@ -3,15 +3,13 @@ package com.futo.platformplayer.encryption
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import com.futo.polycentric.core.EncryptionProvider
import java.security.Key import java.security.Key
import java.security.KeyStore import java.security.KeyStore
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.KeyGenerator import javax.crypto.KeyGenerator
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
class EncryptionProvider { class GEncryptionProviderV0 {
private val _keyStore: KeyStore; private val _keyStore: KeyStore;
private val secretKey: Key? get() = _keyStore.getKey(KEY_ALIAS, null); private val secretKey: Key? get() = _keyStore.getKey(KEY_ALIAS, null);
@ -38,30 +36,31 @@ class EncryptionProvider {
} }
fun encrypt(decrypted: ByteArray): ByteArray { fun encrypt(decrypted: ByteArray): ByteArray {
val c: Cipher = Cipher.getInstance(AES_MODE); 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); val encodedBytes: ByteArray = c.doFinal(decrypted);
return encodedBytes; return encodedBytes;
} }
fun decrypt(encrypted: String): String { fun decrypt(encrypted: String): String {
val c = Cipher.getInstance(AES_MODE); 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))); val decrypted = String(c.doFinal(Base64.decode(encrypted, Base64.DEFAULT)));
return decrypted; return decrypted;
} }
fun decrypt(encrypted: ByteArray): ByteArray { fun decrypt(encrypted: ByteArray): ByteArray {
val c = Cipher.getInstance(AES_MODE); 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); return c.doFinal(encrypted);
} }
companion object { 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 val FIXED_IV = byteArrayOf(12, 43, 127, 2, 99, 22, 6, 78, 24, 53, 8, 101);
private const val AndroidKeyStore = "AndroidKeyStore"; private const val AndroidKeyStore = "AndroidKeyStore";
private const val KEY_ALIAS = "FUTOMedia_Key"; private const val KEY_ALIAS = "FUTOMedia_Key";
private const val AES_MODE = "AES/GCM/NoPadding"; private const val AES_MODE = "AES/GCM/NoPadding";
private val TAG = "EncryptionProvider"; private const val TAG_LENGTH = 128
private val TAG = "GEncryptionProviderV0";
} }
} }

View File

@ -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";
}
}

View File

@ -0,0 +1,8 @@
package com.futo.platformplayer.encryption
class GPasswordEncryptionProvider {
companion object {
val version = 1;
val instance = GPasswordEncryptionProviderV1.instance;
}
}

View File

@ -1,4 +1,3 @@
package com.futo.platformplayer.encryption package com.futo.platformplayer.encryption
import android.util.Base64 import android.util.Base64
@ -6,7 +5,7 @@ import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
class PasswordEncryptionProvider { class GPasswordEncryptionProviderV0 {
private val _key: SecretKeySpec; private val _key: SecretKeySpec;
constructor(password: String) { constructor(password: String) {
@ -20,26 +19,27 @@ class PasswordEncryptionProvider {
} }
fun encrypt(decrypted: ByteArray): ByteArray { fun encrypt(decrypted: ByteArray): ByteArray {
val c: Cipher = Cipher.getInstance(AES_MODE); 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); val encodedBytes: ByteArray = c.doFinal(decrypted);
return encodedBytes; return encodedBytes;
} }
fun decrypt(encrypted: String): String { fun decrypt(encrypted: String): String {
val c = Cipher.getInstance(AES_MODE); 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))); val decrypted = String(c.doFinal(Base64.decode(encrypted, Base64.DEFAULT)));
return decrypted; return decrypted;
} }
fun decrypt(encrypted: ByteArray): ByteArray { fun decrypt(encrypted: ByteArray): ByteArray {
val c = Cipher.getInstance(AES_MODE); 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); return c.doFinal(encrypted);
} }
companion object { companion object {
private val FIXED_IV = byteArrayOf(12, 43, 127, 2, 99, 22, 6, 78, 24, 53, 8, 101); 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 const val AES_MODE = "AES/GCM/NoPadding";
private val TAG = "PasswordEncryptionProvider"; private val TAG = "GPasswordEncryptionProviderV0";
} }
} }

View File

@ -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";
}
}

View File

@ -63,6 +63,7 @@ class ImportSubscriptionsFragment : MainFragment() {
private var _textSelectDeselectAll: TextView; private var _textSelectDeselectAll: TextView;
private var _textNothingToImport: TextView; private var _textNothingToImport: TextView;
private var _textCounter: TextView; private var _textCounter: TextView;
private var _textLoadMore: TextView;
private var _adapterView: AnyAdapterView<SelectableIPlatformChannel, ImportSubscriptionViewHolder>; private var _adapterView: AnyAdapterView<SelectableIPlatformChannel, ImportSubscriptionViewHolder>;
private var _links: List<String> = listOf(); private var _links: List<String> = listOf();
private val _items: ArrayList<SelectableIPlatformChannel> = arrayListOf(); private val _items: ArrayList<SelectableIPlatformChannel> = arrayListOf();
@ -79,6 +80,7 @@ class ImportSubscriptionsFragment : MainFragment() {
_textNothingToImport = findViewById(R.id.nothing_to_import); _textNothingToImport = findViewById(R.id.nothing_to_import);
_textSelectDeselectAll = findViewById(R.id.text_select_deselect_all); _textSelectDeselectAll = findViewById(R.id.text_select_deselect_all);
_textCounter = findViewById(R.id.text_select_counter); _textCounter = findViewById(R.id.text_select_counter);
_textLoadMore = findViewById(R.id.text_load_more);
_spinner = findViewById(R.id.channel_loader); _spinner = findViewById(R.id.channel_loader);
_adapterView = findViewById<RecyclerView>(R.id.recycler_import).asAny( _items) { _adapterView = findViewById<RecyclerView>(R.id.recycler_import).asAny( _items) {
@ -120,6 +122,19 @@ class ImportSubscriptionsFragment : MainFragment() {
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); }); //UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
loadNext(); loadNext();
}; };
_textLoadMore.setOnClickListener {
if (!_limitToastShown) {
return@setOnClickListener;
}
_textLoadMore.visibility = View.GONE;
_limitToastShown = false;
_counter = 0;
load();
};
_textLoadMore.visibility = View.GONE;
} }
fun cleanup() { fun cleanup() {
@ -165,7 +180,8 @@ class ImportSubscriptionsFragment : MainFragment() {
if (_counter >= MAXIMUM_BATCH_SIZE) { if (_counter >= MAXIMUM_BATCH_SIZE) {
if (!_limitToastShown) { if (!_limitToastShown) {
_limitToastShown = true; _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); setLoading(false);

View File

@ -1,29 +1,19 @@
package com.futo.platformplayer.states package com.futo.platformplayer.states
import android.app.Activity
import android.content.Context 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.app.ShareCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.copyTo import com.futo.platformplayer.copyTo
import com.futo.platformplayer.copyToOutputStream import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
import com.futo.platformplayer.encryption.EncryptionProvider import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
import com.futo.platformplayer.encryption.PasswordEncryptionProvider
import com.futo.platformplayer.getInputStream
import com.futo.platformplayer.getNowDiffHours import com.futo.platformplayer.getNowDiffHours
import com.futo.platformplayer.getOutputStream
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.readBytes import com.futo.platformplayer.readBytes
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
@ -39,9 +29,8 @@ import kotlinx.serialization.json.Json
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.InputStream import java.lang.Exception
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@ -83,7 +72,7 @@ class StateBackup {
val pbytes = password.toByteArray(); val pbytes = password.toByteArray();
if(pbytes.size < 4 || pbytes.size > 32) if(pbytes.size < 4 || pbytes.size > 32)
throw IllegalStateException("Automatic backup passwords should atleast be 4 character and smaller than 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 { fun hasAutomaticBackup(): Boolean {
val context = StateApp.instance.contextOrNull ?: return false; val context = StateApp.instance.contextOrNull ?: return false;
@ -107,8 +96,8 @@ class StateBackup {
val data = export(); val data = export();
val zip = data.asZip(); 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)) { if(!Settings.instance.storage.isStorageMainValid(context)) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
UIDialogs.toast("Missing permissions for auto-backup, please set the external general directory in settings"); 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"); throw IllegalStateException("Backup file does not exist");
val backupBytesEncrypted = backupFiles.first!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.first?.uri}]"); val backupBytesEncrypted = backupFiles.first!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.first?.uri}]");
val backupBytes = PasswordEncryptionProvider(getAutomaticBackupPassword(password)).decrypt(backupBytesEncrypted); importEncryptedZipBytes(context, scope, backupBytesEncrypted, password);
importZipBytes(context, scope, backupBytes);
Logger.i(TAG, "Finished AutoBackup restore"); Logger.i(TAG, "Finished AutoBackup restore");
} }
catch (exSec: FileNotFoundException) { catch (exSec: FileNotFoundException) {
@ -180,13 +168,30 @@ class StateBackup {
throw ex; throw ex;
val backupBytesEncrypted = backupFiles.second!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.second?.uri}]"); val backupBytesEncrypted = backupFiles.second!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.second?.uri}]");
val backupBytes = PasswordEncryptionProvider(getAutomaticBackupPassword(password)).decrypt(backupBytesEncrypted); importEncryptedZipBytes(context, scope, backupBytesEncrypted, password);
importZipBytes(context, scope, backupBytes);
Logger.i(TAG, "Finished AutoBackup restore"); 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() { fun startExternalBackup() {
val data = export(); val data = export();
val now = OffsetDateTime.now(); val now = OffsetDateTime.now();

View File

@ -40,6 +40,19 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" /> 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 <TextView
android:id="@+id/text_select_counter" android:id="@+id/text_select_counter"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -637,6 +637,8 @@
<string name="more_options">More Options</string> <string name="more_options">More Options</string>
<string name="save">Save</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="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"> <string-array name="home_screen_array">
<item>Recommendations</item> <item>Recommendations</item>
<item>Subscriptions</item> <item>Subscriptions</item>

@ -1 +1 @@
Subproject commit 3fe81ffb3c730f0dc9f645149dc20c31284cd790 Subproject commit 1b041a8612b98bd0fb7cb0fbb672933674a69035