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