Skip to content

Commit 047e46d

Browse files
committed
finalize encrypted storage
1 parent 3bb1709 commit 047e46d

File tree

5 files changed

+158
-59
lines changed

5 files changed

+158
-59
lines changed

core/src/main/java/com/segment/analytics/kotlin/core/Analytics.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ open class Analytics protected constructor(
4343
}
4444

4545
// use lazy to avoid the instance being leak before fully initialized
46-
val storage: Storage by lazy {
46+
open val storage: Storage by lazy {
4747
configuration.storageProvider.createStorage(this, configuration.application!!)
4848
}
4949

core/src/main/java/com/segment/analytics/kotlin/core/Configuration.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import sovran.kotlin.Store
2525
data class Configuration(
2626
val writeKey: String,
2727
var application: Any? = null,
28-
val storageProvider: StorageProvider = ConcreteStorageProvider,
28+
var storageProvider: StorageProvider = ConcreteStorageProvider,
2929
var collectDeviceId: Boolean = false,
3030
var trackApplicationLifecycleEvents: Boolean = false,
3131
var useLifecycleObserver: Boolean = false,

core/src/main/java/com/segment/analytics/kotlin/core/utilities/EventStream.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ open class InMemoryEventStream: EventStream {
155155
}
156156

157157
open class FileEventStream(
158-
internal val directory: File
158+
val directory: File
159159
): EventStream {
160160

161161
init {
Lines changed: 148 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,192 @@
11
package com.segment.analytics.next
22

3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import com.segment.analytics.kotlin.android.utilities.AndroidKVS
36
import com.segment.analytics.kotlin.core.Analytics
47
import com.segment.analytics.kotlin.core.Storage
58
import com.segment.analytics.kotlin.core.StorageProvider
69
import com.segment.analytics.kotlin.core.utilities.FileEventStream
7-
import com.segment.analytics.kotlin.core.utilities.PropertiesFile
810
import com.segment.analytics.kotlin.core.utilities.StorageImpl
911
import java.io.File
10-
import java.io.FileOutputStream
1112
import java.io.InputStream
12-
import java.security.Key
13+
import java.security.SecureRandom
1314
import javax.crypto.Cipher
1415
import javax.crypto.CipherInputStream
15-
import javax.crypto.CipherOutputStream
16-
import javax.crypto.KeyGenerator
16+
import javax.crypto.spec.IvParameterSpec
17+
import javax.crypto.spec.SecretKeySpec
18+
1719

1820
class EncryptedEventStream(
1921
directory: File,
20-
private val key: Key
22+
val key: ByteArray
2123
) : FileEventStream(directory) {
2224

23-
private var cipherOutputStream: CipherOutputStream? = null
24-
25-
private val encryptedCipher = EncryptionUtil.getCipher(key, Cipher.ENCRYPT_MODE)
26-
27-
override var fs: FileOutputStream?
28-
get() = super.fs
29-
set(value) {
30-
if (value == null) {
31-
cipherOutputStream = null
32-
}
33-
else {
34-
cipherOutputStream = CipherOutputStream(value, encryptedCipher)
35-
}
36-
}
25+
private val ivSize = 16
3726

3827
override fun write(content: String) {
39-
cipherOutputStream?.run {
40-
write(content.toByteArray())
28+
fs?.run {
29+
// generate a different iv for every content
30+
val iv = ByteArray(ivSize).apply {
31+
SecureRandom().nextBytes(this)
32+
}
33+
val cipher = getCipher(Cipher.ENCRYPT_MODE, iv, key)
34+
val encryptedContent = cipher.doFinal(content.toByteArray())
35+
36+
write(iv)
37+
// write the size of the content, so decipher knows
38+
// the length of the content
39+
write(writeInt(encryptedContent.size))
40+
write(encryptedContent)
4141
flush()
4242
}
4343
}
4444

45-
override fun close() {
46-
cipherOutputStream?.close()
47-
super.close()
48-
}
49-
5045
override fun readAsStream(source: String): InputStream? {
5146
val stream = super.readAsStream(source)
5247
return if (stream == null) {
5348
null
5449
} else {
55-
val cipher = EncryptionUtil.getCipher(key, Cipher.DECRYPT_MODE)
56-
CipherInputStream(super.readAsStream(source), cipher)
50+
// the DecryptingInputStream decrypts the steam
51+
// and uses a LimitedInputStream to read the exact
52+
// bytes of a chunk of content
53+
DecryptingInputStream(stream)
54+
}
55+
}
56+
57+
58+
private fun getCipher(mode: Int, iv: ByteArray, key: ByteArray): Cipher {
59+
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
60+
val keySpec = SecretKeySpec(key, "AES")
61+
val ivSpec = IvParameterSpec(iv)
62+
cipher.init(mode, keySpec, ivSpec)
63+
return cipher
64+
}
65+
66+
private fun writeInt(value: Int): ByteArray {
67+
return byteArrayOf(
68+
(value ushr 24).toByte(),
69+
(value ushr 16).toByte(),
70+
(value ushr 8).toByte(),
71+
value.toByte()
72+
)
73+
}
74+
75+
private fun readInt(input: InputStream): Int {
76+
val bytes = input.readNBytes(4)
77+
return (bytes[0].toInt() and 0xFF shl 24) or
78+
(bytes[1].toInt() and 0xFF shl 16) or
79+
(bytes[2].toInt() and 0xFF shl 8) or
80+
(bytes[3].toInt() and 0xFF)
81+
}
82+
83+
private inner class DecryptingInputStream(private val input: InputStream) : InputStream() {
84+
private var currentCipherInputStream: CipherInputStream? = null
85+
private var remainingBytes = 0
86+
private var endOfStream = false
87+
88+
private fun setupNextBlock(): Boolean {
89+
if (endOfStream) return false
90+
91+
try {
92+
// Read IV
93+
val iv = input.readNBytes(ivSize)
94+
if (iv.size < ivSize) {
95+
endOfStream = true
96+
return false
97+
}
98+
99+
// Read content size
100+
remainingBytes = readInt(input)
101+
if (remainingBytes <= 0) {
102+
endOfStream = true
103+
return false
104+
}
105+
106+
// Setup cipher
107+
val cipher = getCipher(Cipher.DECRYPT_MODE, iv, key)
108+
109+
// Create new cipher stream
110+
currentCipherInputStream = CipherInputStream(
111+
LimitedInputStream(input, remainingBytes.toLong()),
112+
cipher
113+
)
114+
return true
115+
} catch (e: Exception) {
116+
endOfStream = true
117+
return false
118+
}
119+
}
120+
121+
override fun read(): Int {
122+
if (currentCipherInputStream == null && !setupNextBlock()) {
123+
return -1
124+
}
125+
126+
val byte = currentCipherInputStream?.read() ?: -1
127+
if (byte == -1) {
128+
currentCipherInputStream = null
129+
return read() // Try next block
130+
}
131+
return byte
132+
}
133+
134+
override fun close() {
135+
currentCipherInputStream?.close()
136+
input.close()
137+
}
138+
}
139+
140+
// Helper class to limit reading to current encrypted block
141+
private class LimitedInputStream(
142+
private val input: InputStream,
143+
private var remaining: Long
144+
) : InputStream() {
145+
override fun read(): Int {
146+
if (remaining <= 0) return -1
147+
val result = input.read()
148+
if (result >= 0) remaining--
149+
return result
150+
}
151+
152+
override fun read(b: ByteArray, off: Int, len: Int): Int {
153+
if (remaining <= 0) return -1
154+
val result = input.read(b, off, minOf(len, remaining.toInt()))
155+
if (result >= 0) remaining -= result
156+
return result
157+
}
158+
159+
override fun close() {
160+
// Don't close the underlying stream
57161
}
58162
}
59163
}
60164

61-
class EncryptedStorageProvider(val key: Key) : StorageProvider {
165+
class EncryptedStorageProvider(val key: ByteArray) : StorageProvider {
62166

63167
override fun createStorage(vararg params: Any): Storage {
64-
if (params.isEmpty() || params[0] !is Analytics) {
65-
throw IllegalArgumentException("Invalid parameters for ConcreteStorageProvider. ConcreteStorageProvider requires at least 1 parameter and the first argument has to be an instance of Analytics")
168+
169+
if (params.size < 2 || params[0] !is Analytics || params[1] !is Context) {
170+
throw IllegalArgumentException("""
171+
Invalid parameters for EncryptedStorageProvider.
172+
EncryptedStorageProvider requires at least 2 parameters.
173+
The first argument has to be an instance of Analytics,
174+
an the second argument has to be an instance of Context
175+
""".trimIndent())
66176
}
67177

68178
val analytics = params[0] as Analytics
179+
val context = params[1] as Context
69180
val config = analytics.configuration
70181

71-
val directory = File("/tmp/analytics-kotlin/${config.writeKey}")
72-
val eventDirectory = File(directory, "events")
182+
val eventDirectory = context.getDir("segment-disk-queue", Context.MODE_PRIVATE)
73183
val fileIndexKey = "segment.events.file.index.${config.writeKey}"
74-
val userPrefs = File(directory, "analytics-kotlin-${config.writeKey}.properties")
184+
val sharedPreferences: SharedPreferences =
185+
context.getSharedPreferences("analytics-android-${config.writeKey}", Context.MODE_PRIVATE)
75186

76-
val propertiesFile = PropertiesFile(userPrefs)
187+
val propertiesFile = AndroidKVS(sharedPreferences)
188+
// use the key from constructor or get it from share preferences
77189
val eventStream = EncryptedEventStream(eventDirectory, key)
78190
return StorageImpl(propertiesFile, eventStream, analytics.store, config.writeKey, fileIndexKey, analytics.fileIODispatcher)
79191
}
80-
}
81-
82-
object EncryptionUtil {
83-
private const val ALGORITHM = "AES"
84-
85-
fun generateKey(): Key {
86-
val keyGen = KeyGenerator.getInstance(ALGORITHM)
87-
keyGen.init(128) // AES 128-bit key
88-
return keyGen.generateKey()
89-
}
90-
91-
fun getCipher(key: Key, mode: Int): Cipher {
92-
val cipher = Cipher.getInstance(ALGORITHM)
93-
cipher.init(mode, key)
94-
return cipher
95-
}
96192
}

samples/kotlin-android-app/src/main/java/com/segment/analytics/next/MainApplication.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,18 @@ class MainApplication : Application() {
2222
override fun onCreate() {
2323
super.onCreate()
2424

25+
// Replace it with your key to the encrypted storage
26+
val secretKey = ByteArray(32) { 1 }
27+
2528
analytics = Analytics("tteOFND0bb5ugJfALOJWpF0wu1tcxYgr", applicationContext) {
2629
this.collectDeviceId = true
2730
this.trackApplicationLifecycleEvents = true
2831
this.trackDeepLinks = true
2932
this.flushPolicies = listOf(
30-
CountBasedFlushPolicy(3), // Flush after 3 events
31-
FrequencyFlushPolicy(5000), // Flush after 5 secs
32-
UnmeteredFlushPolicy(applicationContext) // Flush if network is not metered
33+
CountBasedFlushPolicy(100), // Flush after 3 events
34+
// FrequencyFlushPolicy(60000), // Flush after 5 secs
35+
// UnmeteredFlushPolicy(applicationContext) // Flush if network is not metered
3336
)
34-
this.flushPolicies = listOf(UnmeteredFlushPolicy(applicationContext))
3537
this.requestFactory = object : RequestFactory() {
3638
override fun upload(apiHost: String): HttpURLConnection {
3739
val connection: HttpURLConnection = openConnection("https://$apiHost/b")
@@ -41,6 +43,7 @@ class MainApplication : Application() {
4143
return connection
4244
}
4345
}
46+
this.storageProvider = EncryptedStorageProvider(secretKey)
4447
}
4548
analytics.add(AndroidRecordScreenPlugin())
4649
analytics.add(object : Plugin {

0 commit comments

Comments
 (0)