1 /*
2 * Copyright 2023 Google LLC
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.google.android.gms.nearby.presence.hazmat
17
18 import androidx.annotation.IntDef
19 import com.google.android.gms.nearby.presence.hazmat.LdtNpJni.DecryptErrorCode
20 import com.google.android.gms.nearby.presence.hazmat.LdtNpJni.EncryptErrorCode
21
22 private const val KEY_SEED_SIZE = 32
23 private const val TAG_SIZE = 32
24 private const val BLOCK_SIZE = 16
25
26 // Error return value for create operations
27 private const val CREATE_HANDLE_ERROR = 0L
28
29 // Status code returned on successful cipher operations
30 private const val SUCCESS = 0
31
32 /**
33 * A 2 byte salt that will be used in the advertisement with this encrypted payload.
34 */
35 class Salt(b1: Byte, b2: Byte) {
36 private val saltBytes: Char
37
38 // Returns the bytes of the salt represented as a 2 byte Char type
getBytesAsCharnull39 internal fun getBytesAsChar(): Char {
40 return saltBytes
41 }
42
43 init {
44 // byte widening conversion to int sign-extends
45 val highBits = b1.toInt() shl 8
46 val lowBits = b2.toInt() and 0xFF
47 // narrowing conversion truncates to low 16 bits
48 saltBytes = (highBits or lowBits).toChar()
49 }
50 }
51
52 /** A runtime exception thrown if something fails during a Ldt jni call*/
53 class LdtJniException internal constructor(message: String?) : RuntimeException(message)
54
55 /**
56 * A checked exception which occurs if the calculated hmac tag does not match the expected hmac
57 * tag during decrypt operations
58 */
59 class MacMismatchException internal constructor(message: String?) : Exception(message)
60
61 /**
62 * Create a new LdtEncryptionCipher instance using LDT-XTS-AES128 with the "swap" mix function.
63 *
64 * @constructor Creates a new instance from the provided keySeed
65 * @param keySeed is the key material from the Nearby Presence credential from which
66 * the LDT key will be derived.
67 * @return an instance configured with the supplied key seed
68 * @throws IllegalArgumentException if the keySeed is the wrong size
69 * @throws LdtJniException if creating the instance fails
70 */
71 class LdtEncryptionCipher @Throws(
72 LdtJniException::class,
73 IllegalArgumentException::class
74 ) constructor(keySeed: ByteArray) :
75 AutoCloseable {
76 @Volatile
77 private var closed = false
78 private val handle: Long
79
80 init {
81 require(keySeed.size == KEY_SEED_SIZE)
82 handle = LdtNpJni.createEncryptionCipher(keySeed)
83 if (handle == CREATE_HANDLE_ERROR) {
84 throw LdtJniException("Creating ldt encryption cipher native resources failed")
85 }
86 }
87
88 /**
89 * Encrypt a 16-31 byte buffer in-place.
90 *
91 * @param salt the salt that will be used in the advertisement with this encrypted payload.
92 * @param data plaintext to encrypt in place: the metadata key followed by the data elements to be
93 * encrypted. The length must be in [16, 31).
94 * @throws IllegalStateException if this instance has already been closed
95 * @throws IllegalArgumentException if data is the wrong length
96 * @throws LdtJniException if encryption fails
97 */
98 @Throws(LdtJniException::class, IllegalArgumentException::class, IllegalStateException::class)
encryptnull99 fun encrypt(salt: Salt, data: ByteArray) {
100 check(!closed) { "Use after free! This instance has already been closed" }
101 requireValidSizeData(data.size)
102
103 when (val res = LdtNpJni.encrypt(handle, salt.getBytesAsChar(), data)) {
104 EncryptErrorCode.JNI_OP_ERROR -> throw LdtJniException("Error during jni encrypt operation: error code $res");
105 EncryptErrorCode.DATA_LEN_ERROR -> check(false) // this will never happen, lengths checked above
106 else -> check(res == SUCCESS)
107 }
108 }
109
110 /**
111 * Releases native resources.
112 *
113 * <p>Once closed, a Ldt instance cannot be used further.
114 */
closenull115 override fun close() {
116 if (!closed) {
117 closed = true
118 LdtNpJni.closeEncryptCipher(handle)
119 }
120 }
121 }
122
123 /**
124 * A LdtDecryptionCipher instance which uses LDT-XTS-AES128 with the "swap" mix function.
125 *
126 * @constructor Creates a new instance from the provided keySeed and hmacTag
127 * @param keySeed is the key material from the Nearby Presence credential from which
128 * the LDT key will be derived.
129 * @param hmacTag is the hmac auth tag calculated on the metadata key used to verify
130 * decryption was successful.
131 * @return an instance configured with the supplied key seed
132 * @throws IllegalArgumentException if the keySeed is the wrong size
133 * @throws IllegalArgumentException if the hmacTag is the wrong size
134 * @throws LdtJniException if creating the instance fails
135 */
136 class LdtDecryptionCipher @Throws(
137 LdtJniException::class,
138 IllegalArgumentException::class
139 ) constructor(keySeed: ByteArray, hmacTag: ByteArray) : AutoCloseable {
140 @Volatile
141 private var closed = false
142 private val handle: Long
143
144 init {
145 require(keySeed.size == KEY_SEED_SIZE)
146 require(hmacTag.size == TAG_SIZE)
147 handle = LdtNpJni.createDecryptionCipher(keySeed, hmacTag)
148 if (handle == CREATE_HANDLE_ERROR) {
149 throw LdtJniException("Creating ldt decryption cipher native resources failed")
150 }
151 }
152
153 /** Error codes which map to return values on the native side. */
154 @IntDef(DecryptAndVerifyResultCode.SUCCESS, DecryptAndVerifyResultCode.MAC_MISMATCH)
155 annotation class DecryptAndVerifyResultCode {
156 companion object {
157 const val SUCCESS = 0
158 const val MAC_MISMATCH = -1
159 }
160 }
161
162 /**
163 * Decrypt a 16-31 byte buffer in-place and verify the plaintext metadata key matches
164 * this item's MAC, if not the buffer will not be decrypted.
165 *
166 * @param salt the salt extracted from the advertisement that contained this payload.
167 * @param data ciphertext to decrypt in place: the metadata key followed by the data elements to
168 * be decrypted. The length must be in [16, 31).
169 * @return a [DecryptAndVerifyResultCode] indicating of the decrypt operation failed or succeeded.
170 * In the case of a failed decrypt, the provided plaintext will not change.
171 * @throws IllegalStateException if this instance has already been closed
172 * @throws IllegalArgumentException if data is the wrong length
173 * @throws LdtJniException if decryption fails
174 */
175 @Throws(
176 IllegalStateException::class,
177 IllegalArgumentException::class,
178 LdtJniException::class
179 )
180 @DecryptAndVerifyResultCode
decryptAndVerifynull181 fun decryptAndVerify(salt: Salt, data: ByteArray): Int {
182 check(!closed) { "Double free! Close should only ever be called once" }
183 requireValidSizeData(data.size)
184
185 when (val res = LdtNpJni.decryptAndVerify(handle, salt.getBytesAsChar(), data)) {
186 DecryptErrorCode.MAC_MISMATCH -> return DecryptAndVerifyResultCode.MAC_MISMATCH
187 DecryptErrorCode.DATA_LEN_ERROR -> check(false); // This condition is impossible, we validated data length above
188 DecryptErrorCode.JNI_OP_ERROR -> throw LdtJniException("Error occurred during jni encrypt operation")
189 else -> check(res == SUCCESS)
190 }
191 return DecryptAndVerifyResultCode.SUCCESS
192 }
193
194 /**
195 * Releases native resources.
196 *
197 * <p>Once closed, a Ldt instance cannot be used further.
198 */
closenull199 override fun close() {
200 if (!closed) {
201 closed = true
202 LdtNpJni.closeEncryptCipher(handle)
203 }
204 }
205 }
206
requireValidSizeDatanull207 private fun requireValidSizeData(size: Int) {
208 require(size >= BLOCK_SIZE && size < BLOCK_SIZE * 2) { "Invalid size data: $size" }
209 }
210
211