Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.harrytmthy.safebox.SafeBox.Companion.DEFAULT_KEY_ALIAS
import com.harrytmthy.safebox.state.SafeBoxStateListener
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
import org.junit.After
import org.junit.runner.RunWith
import java.io.File
import java.util.concurrent.atomic.AtomicReference
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
Expand All @@ -48,9 +52,45 @@ class SafeBoxTest {

@After
fun tearDown() {
safeBox.edit()
.clear()
.commit()
SafeBox.getOrNull(fileName)
?.edit()
?.clear()
?.commit()

File(context.noBackupFilesDir, "$fileName.bin").delete()
File(context.noBackupFilesDir, "$DEFAULT_KEY_ALIAS.bin").delete()
}

@Test
fun create_then_get_shouldReturnSameInstance() {
safeBox = createSafeBox()

val instance = SafeBox.get(fileName)

assertTrue(safeBox === instance)
}

@Test
fun create_withConcurrentCalls_shouldReturnSameInstance() = runTest {
val createdInstance = AtomicReference<SafeBox>()
repeat(10) {
launch(Dispatchers.IO) {
val newInstance = createSafeBox()
val previousInstance = createdInstance.getAndSet(newInstance) ?: return@launch
assertTrue(newInstance === previousInstance)
}
}
}

@Test
fun create_withDifferentFileName_shouldReturnDifferentInstances() {
safeBox = createSafeBox()

val anotherSafeBox = createSafeBox(fileName = "test_safebox.bin")

assertTrue(safeBox !== anotherSafeBox)
assertTrue(safeBox === createSafeBox())
File(context.noBackupFilesDir, "test_safebox.bin").delete()
}

@Test
Expand Down Expand Up @@ -187,6 +227,7 @@ class SafeBoxTest {
}

private fun createSafeBox(
fileName: String = this.fileName,
ioDispatcher: CoroutineDispatcher = UnconfinedTestDispatcher(),
stateListener: SafeBoxStateListener? = null,
): SafeBox =
Expand Down
72 changes: 51 additions & 21 deletions safebox/src/main/java/com/harrytmthy/safebox/SafeBox.kt
Original file line number Diff line number Diff line change
Expand Up @@ -418,23 +418,29 @@ public class SafeBox private constructor(
stateListener?.let(safeBox.stateManager::setStateListener)
return safeBox
}
val aesGcmCipherProvider = AesGcmCipherProvider.create(
alias = valueKeyStoreAlias,
aad = additionalAuthenticatedData,
)
val keyProvider = SecureRandomKeyProvider.create(
context = context,
fileName = keyAlias,
keySize = ChaCha20CipherProvider.KEY_SIZE,
algorithm = ChaCha20CipherProvider.ALGORITHM,
cipherProvider = aesGcmCipherProvider,
)
val keyCipherProvider = ChaCha20CipherProvider(keyProvider, deterministic = true)
val valueCipherProvider = ChaCha20CipherProvider(keyProvider, deterministic = false)
val stateManager = SafeBoxStateManager(fileName, stateListener, ioDispatcher)
val blobStore = SafeBoxBlobStore.create(context, fileName)
return SafeBox(blobStore, keyCipherProvider, valueCipherProvider, stateManager)
.also { instances[fileName] = it }
return synchronized(instances) {
instances[fileName]?.let { safeBox ->
stateListener?.let(safeBox.stateManager::setStateListener)
return safeBox
}
val aesGcmCipherProvider = AesGcmCipherProvider.create(
alias = valueKeyStoreAlias,
aad = additionalAuthenticatedData,
)
val keyProvider = SecureRandomKeyProvider.create(
context = context,
fileName = keyAlias,
keySize = ChaCha20CipherProvider.KEY_SIZE,
algorithm = ChaCha20CipherProvider.ALGORITHM,
cipherProvider = aesGcmCipherProvider,
)
val keyCipherProvider = ChaCha20CipherProvider(keyProvider, deterministic = true)
val valueCipherProvider = ChaCha20CipherProvider(keyProvider, deterministic = false)
val stateManager = SafeBoxStateManager(fileName, stateListener, ioDispatcher)
val blobStore = SafeBoxBlobStore.create(context, fileName)
SafeBox(blobStore, keyCipherProvider, valueCipherProvider, stateManager)
.also { instances[fileName] = it }
}
}

/**
Expand Down Expand Up @@ -472,10 +478,34 @@ public class SafeBox private constructor(
stateListener?.let(safeBox.stateManager::setStateListener)
return safeBox
}
val stateManager = SafeBoxStateManager(fileName, stateListener, ioDispatcher)
val blobStore = SafeBoxBlobStore.create(context, fileName)
return SafeBox(blobStore, keyCipherProvider, valueCipherProvider, stateManager)
.also { instances[fileName] = it }
return synchronized(instances) {
instances[fileName]?.let { safeBox ->
stateListener?.let(safeBox.stateManager::setStateListener)
return safeBox
}
val stateManager = SafeBoxStateManager(fileName, stateListener, ioDispatcher)
val blobStore = SafeBoxBlobStore.create(context, fileName)
SafeBox(blobStore, keyCipherProvider, valueCipherProvider, stateManager)
.also { instances[fileName] = it }
}
}

/**
* Returns the previously created instance for [fileName].
*
* @return The existing [SafeBox] instance.
* @throws IllegalStateException if [create] has not been called for this file.
*/
@JvmStatic
public fun get(fileName: String): SafeBox =
instances[fileName] ?: error("SafeBox '$fileName' is not initialized.")

/**
* Returns the previously created instance for [fileName], or null if not initialized.
*
* @return The existing [SafeBox] instance, or null.
*/
@JvmStatic
public fun getOrNull(fileName: String): SafeBox? = instances[fileName]
}
}