1 /*
2  * Copyright (C) 2019 The Android Open Source Project
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 
17 package com.android.car.trust;
18 
19 import android.util.Log;
20 
21 import com.android.car.BLEStreamProtos.BLEMessageProto.BLEMessage;
22 import com.android.car.BLEStreamProtos.BLEOperationProto.OperationType;
23 import com.android.car.protobuf.ByteString;
24 import com.android.internal.annotations.VisibleForTesting;
25 
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.List;
29 
30 /**
31  * Methods for creating {@link BLEStream} protos
32  */
33 class BLEMessageV1Factory {
34 
35     private static final String TAG = "BLEMessageFactory";
36 
37     /**
38      * The size in bytes of a {@code fixed32} field in the proto.
39      * See this <a href="https://developers.google.com/protocol-buffers/docs/encoding">site</a> for
40      * more details.
41      */
42     private static final int FIXED_32_SIZE = 4;
43 
44     // The size needed to encode a boolean proto field
45     private static final int BOOLEAN_FIELD_ENCODING_SIZE = 1;
46 
47     /**
48      * The bytes needed to encode the field number in the proto.
49      *
50      * <p>Since the v1 proto only has 6 fields, it will only take 1 additional byte to encode.
51      */
52     private static final int FIELD_NUMBER_ENCODING_SIZE = 1;
53     /**
54      * Current version of the proto.
55      */
56     private static final int PROTOCOL_VERSION = 1;
57     /**
58      * The size of the version field in the proto.
59      *
60      * <p>The version field is a {@code variant} and thus, its size can vary between 1-5 bytes.
61      * Since the version is {@code 1}, it will only take 1 byte to encode + 1 byte for the field
62      * number.
63      */
64     private static final int VERSION_SIZE = getEncodedSize(PROTOCOL_VERSION)
65             + FIELD_NUMBER_ENCODING_SIZE;
66     /**
67      * The size of fields in the header that do not change depending on their value.
68      *
69      * <p>The fixed fields are:
70      *
71      * <ol>
72      * <li>Version (1 byte + 1 byte for field)
73      * <li>Packet number (4 bytes + 1 byte for field)
74      * <li>Total packets (4 bytes + 1 byte for field)
75      * </ol>
76      *
77      * <p>Note, the version code is an {@code Int32Value} and thus, can vary, but it is always a set
78      * value at compile time. Thus, it can be calculated statically.
79      */
80     private static final int CONSTANT_HEADER_FIELD_SIZE = VERSION_SIZE
81             + (FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE)
82             + (FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE);
83 
BLEMessageV1Factory()84     private BLEMessageV1Factory() {}
85 
86     /**
87      * Creates an acknowledgement {@link BLEMessage}.
88      *
89      * <p>This type of proto should be used to let a client know that this device has received
90      * a partially completed {@code BLEMessage}.
91      *
92      * <p>Note, this type of message has an empty {@code payload} field.
93      *
94      * @return A {@code BLEMessage} with an {@code OperationType} of {@link OperationType.ACK}.
95      */
makeAcknowledgementMessage()96     static BLEMessage makeAcknowledgementMessage() {
97         return BLEMessage.newBuilder()
98                 .setVersion(PROTOCOL_VERSION)
99                 .setOperation(OperationType.ACK)
100                 .setPacketNumber(1)
101                 .setTotalPackets(1)
102                 .setIsPayloadEncrypted(false)
103                 .build();
104     }
105 
106     /**
107      * Method used to generate a single message, the packet number and total packets will set to 1
108      * by default
109      *
110      * @param payload   The data object to use as the
111      *                  {@link com.android.car.trust.BLEStream.BLEMessage}
112      *                  payload
113      * @param operation The operation this message represents
114      * @return The generated {@link com.android.car.trust.BLEStream.BLEMessage}
115      */
116     @VisibleForTesting
makeBLEMessage(byte[] payload, OperationType operation, boolean isPayloadEncrypted)117     static BLEMessage makeBLEMessage(byte[] payload, OperationType operation,
118             boolean isPayloadEncrypted) {
119         return BLEMessage.newBuilder()
120                 .setVersion(PROTOCOL_VERSION)
121                 .setOperation(operation)
122                 .setPacketNumber(1)
123                 .setTotalPackets(1)
124                 .setIsPayloadEncrypted(isPayloadEncrypted)
125                 .setPayload(ByteString.copyFrom(payload))
126                 .build();
127     }
128 
129     /**
130      * Split given data if necessary to fit within the given {@code maxSize}
131      *
132      * @param payload   The payload to potentially split across multiple {@link
133      *                  com.android.car.trust.BLEStream.BLEMessage}s
134      * @param operation The operation this message represents
135      * @param maxSize   The maximum size of each chunk
136      * @return An array of {@link com.android.car.trust.BLEStream.BLEMessage}s
137      */
makeBLEMessages(byte[] payload, OperationType operation, int maxSize, boolean isPayloadEncrypted)138     public static List<BLEMessage> makeBLEMessages(byte[] payload, OperationType operation,
139             int maxSize, boolean isPayloadEncrypted) {
140         List<BLEMessage> bleMessages = new ArrayList();
141         int payloadSize = payload.length;
142         int maxPayloadSize =
143                 maxSize - getProtoHeaderSize(operation, payloadSize, isPayloadEncrypted);
144         if (payloadSize <= maxPayloadSize) {
145             bleMessages.add(makeBLEMessage(payload, operation, isPayloadEncrypted));
146             return bleMessages;
147         }
148         int totalPackets = (int) Math.ceil((double) payloadSize / maxPayloadSize);
149         int start = 0;
150         int end = maxPayloadSize;
151         for (int i = 0; i < totalPackets; i++) {
152             bleMessages.add(BLEMessage.newBuilder()
153                     .setVersion(PROTOCOL_VERSION)
154                     .setOperation(operation)
155                     .setPacketNumber(i + 1)
156                     .setTotalPackets(totalPackets)
157                     .setIsPayloadEncrypted(isPayloadEncrypted)
158                     .setPayload(ByteString.copyFrom(Arrays.copyOfRange(payload, start, end)))
159                     .build());
160             start = end;
161             end = Math.min(start + maxPayloadSize, payloadSize);
162         }
163         return bleMessages;
164     }
165 
166     /**
167      * Returns the header size for the proto in bytes. This method assumes that the proto
168      * contain a payload.
169      */
170     @VisibleForTesting
getProtoHeaderSize(OperationType operation, int payloadSize, boolean isPayloadEncrypted)171     static int getProtoHeaderSize(OperationType operation, int payloadSize,
172             boolean isPayloadEncrypted) {
173         int isPayloadEncryptedFieldSize = isPayloadEncrypted
174                 ? BOOLEAN_FIELD_ENCODING_SIZE + FIELD_NUMBER_ENCODING_SIZE
175                 : 0;
176         int operationSize = getEncodedSize(operation.getNumber()) + FIELD_NUMBER_ENCODING_SIZE;
177 
178         // The payload size is a varint.
179         int payloadEncodingSize = FIELD_NUMBER_ENCODING_SIZE + getEncodedSize(payloadSize);
180 
181         return CONSTANT_HEADER_FIELD_SIZE + operationSize + isPayloadEncryptedFieldSize
182                 + payloadEncodingSize;
183     }
184 
185     /**
186      * The methods in this section are taken from
187      * google3/third_party/swift/swift_protobuf/Sources/SwiftProtobuf/Variant.swift.
188      * It should be kept in sync as long as the proto version remains the same.
189      *
190      * <p>Computes the number of bytes that would be needed to store a 32-bit variant. Negative
191      * value is not considered because all proto values should be positive.
192      *
193      * @param value the data that need to be encoded
194      * @return the size of the encoded data
195      */
getEncodedSize(int value)196     private static int getEncodedSize(int value) {
197         if (value < 0) {
198             Log.e(TAG, "Get a negative value from proto");
199             return 10;
200         }
201         if ((value & (~0 << 7)) == 0) {
202             return 1;
203         }
204         if ((value & (~0 << 14)) == 0) {
205             return 2;
206         }
207         if ((value & (~0 << 21)) == 0) {
208             return 3;
209         }
210         if ((value & (~0 << 28)) == 0) {
211             return 4;
212         }
213         return 5;
214     }
215 }
216