Initial commit
This initial commit includes HUSH specific changes starting at this commit:
d14637012c
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package cash.z.ecc.android.lockbox
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LockBoxText {
|
||||
|
||||
private lateinit var appContext: Context
|
||||
private lateinit var lockBox: LockBoxProvider
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
lockBox = LockBox(appContext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSeed_store() {
|
||||
val testMessage = "Some Bytes To Test"
|
||||
val testBytes = testMessage.toByteArray()
|
||||
lockBox.setBytes("seed", testBytes)
|
||||
assertEquals(testMessage, String(lockBox.getBytes("seed")!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSeed_storeNegatives() {
|
||||
val testBytes = byteArrayOf(0x00, 0x00, -0x0F, -0x0B)
|
||||
lockBox.setBytes("seed", testBytes)
|
||||
assertTrue(testBytes.contentEquals(lockBox.getBytes("seed")!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSeed_storeLeadingZeros() {
|
||||
val testBytes = byteArrayOf(0x00, 0x00, 0x0F, 0x0B)
|
||||
lockBox.setBytes("seed", testBytes)
|
||||
assertTrue(testBytes.contentEquals(lockBox.getBytes("seed")!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPrivateKey_retrieve() {
|
||||
val testMessage = "Some Bytes To Test"
|
||||
lockBox.setCharsUtf8("spendingKey", testMessage.toCharArray())
|
||||
assertEquals(testMessage, String(lockBox.getCharsUtf8("spendingKey")!!))
|
||||
}
|
||||
}
|
||||
2
lockbox/src/main/AndroidManifest.xml
Normal file
2
lockbox/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
133
lockbox/src/main/java/cash/z/ecc/android/lockbox/LockBox.kt
Normal file
133
lockbox/src/main/java/cash/z/ecc/android/lockbox/LockBox.kt
Normal file
@@ -0,0 +1,133 @@
|
||||
package cash.z.ecc.android.lockbox
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.android.plugin.LockBoxPlugin
|
||||
import de.adorsys.android.securestoragelibrary.SecurePreferences
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.CharBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.*
|
||||
|
||||
class LockBox(private val appContext: Context) : LockBoxPlugin {
|
||||
|
||||
private val maxLength: Int = 50
|
||||
|
||||
override fun setBoolean(key: String, value: Boolean) {
|
||||
setChunkedString(key, value.toString())
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String): Boolean {
|
||||
return getChunkedString(key)?.toBoolean() ?: false
|
||||
}
|
||||
|
||||
override fun setBytes(key: String, value: ByteArray) {
|
||||
// using hex here because this library doesn't really work well for byte arrays
|
||||
// but hopefully we can code to arrays and then change the underlying library, later
|
||||
setChunkedString(key, value.toHex())
|
||||
}
|
||||
|
||||
override fun getBytes(key: String): ByteArray? {
|
||||
return getChunkedString(key)?.fromHex()
|
||||
}
|
||||
|
||||
override fun setCharsUtf8(key: String, value: CharArray) {
|
||||
// Using string here because this library doesn't work well for char arrays
|
||||
// but hopefully we can code to arrays and then change the underlying library, later
|
||||
setChunkedString(key, String(value))
|
||||
}
|
||||
|
||||
override fun getCharsUtf8(key: String): CharArray? {
|
||||
return getChunkedString(key)?.toCharArray()
|
||||
}
|
||||
|
||||
fun delete(key: String) {
|
||||
return SecurePreferences.removeValue(appContext, key)
|
||||
}
|
||||
fun clear() {
|
||||
SecurePreferences.clearAllValues(appContext)
|
||||
}
|
||||
|
||||
inline operator fun <reified T> set(key: String, value: T) {
|
||||
when (T::class) {
|
||||
Boolean::class -> setBoolean(key, value as Boolean)
|
||||
ByteArray::class -> setBytes(key, value as ByteArray)
|
||||
CharArray::class -> setCharsUtf8(key, value as CharArray)
|
||||
Double::class, Float::class, Integer::class, Long::class, String::class -> setChunkedString(key, value.toString())
|
||||
else -> throw UnsupportedOperationException("Lockbox does not yet support setting ${T::class.java.simpleName} objects but it can easily be added.")
|
||||
}
|
||||
}
|
||||
|
||||
inline operator fun <reified T> get(key: String): T? = when (T::class) {
|
||||
Boolean::class -> getBoolean(key)
|
||||
ByteArray::class -> getBytes(key)
|
||||
CharArray::class -> getCharsUtf8(key)
|
||||
Double::class -> getChunkedString(key)?.let { it.toDoubleOrNull() }
|
||||
Float::class -> getChunkedString(key)?.let { it.toFloatOrNull() }
|
||||
Integer::class -> getChunkedString(key)?.let { it.toIntOrNull() }
|
||||
Long::class -> getChunkedString(key)?.let { it.toLongOrNull() }
|
||||
String::class -> getChunkedString(key)
|
||||
else -> throw UnsupportedOperationException("Lockbox does not yet support getting ${T::class.simpleName} objects but it can easily be added")
|
||||
} as T
|
||||
|
||||
/**
|
||||
* Splits a string value into smaller pieces so as not to exceed the limit on the length of
|
||||
* String that can be stored.
|
||||
*/
|
||||
fun setChunkedString(key: String, value: String) {
|
||||
if (value.length > maxLength) {
|
||||
SecurePreferences.setValue(appContext, key, value.chunked(maxLength))
|
||||
} else {
|
||||
SecurePreferences.setValue(appContext, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string value from storage by first fetching the key, directly. If that is missing,
|
||||
* it checks for a chunked version of the key. If that exists, it will be merged and returned.
|
||||
* If not, then null will be returned.
|
||||
*
|
||||
* @return the key if found and null otherwise.
|
||||
*/
|
||||
fun getChunkedString(key: String): String? {
|
||||
return SecurePreferences.getStringValue(appContext, key, null)
|
||||
?: SecurePreferences.getStringListValue(appContext, key, listOf()).let { result ->
|
||||
if (result.size == 0) null else result.joinToString("")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Extensions (TODO: find library that works better with arrays of bytes and chars)
|
||||
//
|
||||
|
||||
private fun ByteArray.toHex(): String {
|
||||
val sb = StringBuilder(size * 2)
|
||||
for (b in this)
|
||||
sb.append(String.format("%02x", b))
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun String.fromHex(): ByteArray {
|
||||
val len = length
|
||||
val data = ByteArray(len / 2)
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
data[i / 2] =
|
||||
((Character.digit(this[i], 16) shl 4) + Character.digit(this[i + 1], 16)).toByte()
|
||||
i += 2
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private fun CharArray.toBytes(): ByteArray {
|
||||
val byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(this))
|
||||
return Arrays.copyOf(byteBuffer.array(), byteBuffer.limit());
|
||||
}
|
||||
|
||||
private fun ByteArray.fromBytes(): CharArray {
|
||||
val charBuffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(this))
|
||||
return Arrays.copyOf(charBuffer.array(), charBuffer.limit())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user