1
1
package com.segment.analytics.next
2
2
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import com.segment.analytics.kotlin.android.utilities.AndroidKVS
3
6
import com.segment.analytics.kotlin.core.Analytics
4
7
import com.segment.analytics.kotlin.core.Storage
5
8
import com.segment.analytics.kotlin.core.StorageProvider
6
9
import com.segment.analytics.kotlin.core.utilities.FileEventStream
7
- import com.segment.analytics.kotlin.core.utilities.PropertiesFile
8
10
import com.segment.analytics.kotlin.core.utilities.StorageImpl
9
11
import java.io.File
10
- import java.io.FileOutputStream
11
12
import java.io.InputStream
12
- import java.security.Key
13
+ import java.security.SecureRandom
13
14
import javax.crypto.Cipher
14
15
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
+
17
19
18
20
class EncryptedEventStream (
19
21
directory : File ,
20
- private val key : Key
22
+ val key : ByteArray
21
23
) : FileEventStream(directory) {
22
24
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
37
26
38
27
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)
41
41
flush()
42
42
}
43
43
}
44
44
45
- override fun close () {
46
- cipherOutputStream?.close()
47
- super .close()
48
- }
49
-
50
45
override fun readAsStream (source : String ): InputStream ? {
51
46
val stream = super .readAsStream(source)
52
47
return if (stream == null ) {
53
48
null
54
49
} 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
57
161
}
58
162
}
59
163
}
60
164
61
- class EncryptedStorageProvider (val key : Key ) : StorageProvider {
165
+ class EncryptedStorageProvider (val key : ByteArray ) : StorageProvider {
62
166
63
167
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())
66
176
}
67
177
68
178
val analytics = params[0 ] as Analytics
179
+ val context = params[1 ] as Context
69
180
val config = analytics.configuration
70
181
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 )
73
183
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 )
75
186
76
- val propertiesFile = PropertiesFile (userPrefs)
187
+ val propertiesFile = AndroidKVS (sharedPreferences)
188
+ // use the key from constructor or get it from share preferences
77
189
val eventStream = EncryptedEventStream (eventDirectory, key)
78
190
return StorageImpl (propertiesFile, eventStream, analytics.store, config.writeKey, fileIndexKey, analytics.fileIODispatcher)
79
191
}
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
- }
96
192
}
0 commit comments