From 1078011691fe69e79eb12c26602fb3f14d5375ec Mon Sep 17 00:00:00 2001 From: Yufeng Wang Date: Thu, 22 Jun 2023 17:22:50 -0700 Subject: [PATCH] Implement kotlin setup-payload phase III (#27389) --- .../google/chip/chiptool/CHIPToolActivity.kt | 2 +- .../setuppayloadscanner/CHIPDeviceInfo.kt | 2 +- scripts/build/builders/android.py | 12 +- .../dry_run_android-arm64-chip-tool.txt | 2 - src/controller/java/BUILD.gn | 27 +- .../java/OnboardingPayloadParser-JNI.cpp | 337 ------------------ .../java/src/chip/onboardingpayload/Base38.kt | 193 ++++++++++ .../onboardingpayload/OnboardingPayload.kt | 308 +++++++++++++++- .../OnboardingPayloadParser.kt | 19 +- .../onboardingpayload/OptionalQRCodeInfo.kt | 17 +- .../QRCodeBasicOnboardingPayloadGenerator.kt | 145 ++++++++ .../QRCodeOnboardingPayloadGenerator.kt | 186 ++++++++++ .../QRCodeOnboardingPayloadParser.kt | 122 +++++++ 13 files changed, 988 insertions(+), 384 deletions(-) delete mode 100644 src/controller/java/OnboardingPayloadParser-JNI.cpp create mode 100644 src/controller/java/src/chip/onboardingpayload/Base38.kt create mode 100644 src/controller/java/src/chip/onboardingpayload/QRCodeBasicOnboardingPayloadGenerator.kt create mode 100644 src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadGenerator.kt create mode 100644 src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadParser.kt diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/CHIPToolActivity.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/CHIPToolActivity.kt index 59c75e2a976a59..07bde0bf7dac57 100644 --- a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/CHIPToolActivity.kt +++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/CHIPToolActivity.kt @@ -210,7 +210,7 @@ class CHIPToolActivity : // parse payload from JSON val setupPayload = OnboardingPayload() // set defaults - setupPayload.discoveryCapabilities = setOf() + setupPayload.discoveryCapabilities = mutableSetOf() setupPayload.optionalQRCodeInfo = HashMap() // read from payload diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/setuppayloadscanner/CHIPDeviceInfo.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/setuppayloadscanner/CHIPDeviceInfo.kt index fc57a96c222ab5..39909cbee1dd84 100644 --- a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/setuppayloadscanner/CHIPDeviceInfo.kt +++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/setuppayloadscanner/CHIPDeviceInfo.kt @@ -33,7 +33,7 @@ data class CHIPDeviceInfo( val setupPinCode: Long = 0L, var commissioningFlow: Int = 0, val optionalQrCodeInfoMap: Map = mapOf(), - val discoveryCapabilities: Set = setOf(), + val discoveryCapabilities: MutableSet = mutableSetOf(), val isShortDiscriminator: Boolean = false, val ipAddress: String? = null, ) : Parcelable { diff --git a/scripts/build/builders/android.py b/scripts/build/builders/android.py index bab58f3f8b7b6e..7e0ec61bc7290c 100644 --- a/scripts/build/builders/android.py +++ b/scripts/build/builders/android.py @@ -210,7 +210,6 @@ def copyToSrcAndroid(self): # If we unify the JNI libraries, libc++_shared.so may not be needed anymore, which could # be another path of resolving this inconsistency. for libName in [ - "libOnboardingPayload.so", "libCHIPController.so", "libc++_shared.so", ]: @@ -478,8 +477,7 @@ def _build(self): self.root, "examples/", self.app.ExampleName(), "android/App/app/libs" ) - libs = ["libOnboardingPayload.so", - "libc++_shared.so", "libTvApp.so"] + libs = ["libc++_shared.so", "libTvApp.so"] jars = { "OnboardingPayload.jar": "third_party/connectedhomeip/src/controller/java/OnboardingPayload.jar", @@ -540,14 +538,6 @@ def build_outputs(self): "lib", "src/controller/java/OnboardingPayload.jar", ), - "jni/%s/libOnboardingPayload.so" - % self.board.AbiName(): os.path.join( - self.output_dir, - "lib", - "jni", - self.board.AbiName(), - "libOnboardingPayload.so", - ), "jni/%s/libCHIPController.so" % self.board.AbiName(): os.path.join( self.output_dir, diff --git a/scripts/build/testdata/dry_run_android-arm64-chip-tool.txt b/scripts/build/testdata/dry_run_android-arm64-chip-tool.txt index 6adfec53c647bf..b62ee9eb522803 100644 --- a/scripts/build/testdata/dry_run_android-arm64-chip-tool.txt +++ b/scripts/build/testdata/dry_run_android-arm64-chip-tool.txt @@ -19,8 +19,6 @@ ninja -C {out}/android-arm64-chip-tool # Prepare Native libs android-arm64-chip-tool mkdir -p {root}/examples/android/CHIPTool/app/libs/jniLibs/arm64-v8a -cp {out}/android-arm64-chip-tool/lib/jni/arm64-v8a/libOnboardingPayload.so {root}/examples/android/CHIPTool/app/libs/jniLibs/arm64-v8a/libOnboardingPayload.so - cp {out}/android-arm64-chip-tool/lib/jni/arm64-v8a/libCHIPController.so {root}/examples/android/CHIPTool/app/libs/jniLibs/arm64-v8a/libCHIPController.so cp {out}/android-arm64-chip-tool/lib/jni/arm64-v8a/libc++_shared.so {root}/examples/android/CHIPTool/app/libs/jniLibs/arm64-v8a/libc++_shared.so diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn index 6b462d85fd85be..ba6b7e958c9b3f 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -238,33 +238,13 @@ kotlin_library("json_to_tlv_to_json_test") { kotlinc_flags = [ "-Xlint:deprecation" ] } -shared_library("jni_for_onboarding_payload") { - output_name = "libOnboardingPayload" - if (matter_enable_java_compilation) { - include_dirs = java_matter_controller_dependent_paths - output_dir = "${root_out_dir}/lib/jni" - } else { - output_dir = "${root_out_dir}/lib/jni/${android_abi}" - } - - sources = [ "OnboardingPayloadParser-JNI.cpp" ] - - deps = [ - "${chip_root}/src/lib", - "${chip_root}/src/setup_payload", - ] -} - kotlin_library("onboarding_payload") { output_name = "OnboardingPayload.jar" - data_deps = [ ":jni_for_onboarding_payload" ] - - if (!matter_enable_java_compilation) { - data_deps += [ "${chip_root}/build/chip/java:shared_cpplib" ] - } + deps = [ ":tlv" ] sources = [ + "src/chip/onboardingpayload/Base38.kt", "src/chip/onboardingpayload/CommissioningFlow.kt", "src/chip/onboardingpayload/DiscoveryCapability.kt", "src/chip/onboardingpayload/ManualOnboardingPayloadGenerator.kt", @@ -272,6 +252,9 @@ kotlin_library("onboarding_payload") { "src/chip/onboardingpayload/OnboardingPayload.kt", "src/chip/onboardingpayload/OnboardingPayloadParser.kt", "src/chip/onboardingpayload/OptionalQRCodeInfo.kt", + "src/chip/onboardingpayload/QRCodeBasicOnboardingPayloadGenerator.kt", + "src/chip/onboardingpayload/QRCodeOnboardingPayloadGenerator.kt", + "src/chip/onboardingpayload/QRCodeOnboardingPayloadParser.kt", "src/chip/onboardingpayload/VendorId.kt", "src/chip/onboardingpayload/Verhoeff.kt", "src/chip/onboardingpayload/Verhoeff10.kt", diff --git a/src/controller/java/OnboardingPayloadParser-JNI.cpp b/src/controller/java/OnboardingPayloadParser-JNI.cpp deleted file mode 100644 index 1c484eca5142a5..00000000000000 --- a/src/controller/java/OnboardingPayloadParser-JNI.cpp +++ /dev/null @@ -1,337 +0,0 @@ -#include "lib/core/CHIPError.h" -#include "lib/support/JniTypeWrappers.h" -#include -#include - -#include -#include -#include -#include - -#include - -#include - -using namespace chip; - -#define SETUP_PAYLOAD_PARSER_JNI_ERROR_MIN 10 // avoiding collision with CHIPJNIError.h -#define _SETUP_PAYLOAD_PARSER_JNI_ERROR(e) CHIP_APPLICATION_ERROR(SETUP_PAYLOAD_PARSER_JNI_ERROR_MIN + (e)) - -#define SETUP_PAYLOAD_PARSER_JNI_ERROR_EXCEPTION_THROWN _SETUP_PAYLOAD_PARSER_JNI_ERROR(0) -#define SETUP_PAYLOAD_PARSER_JNI_ERROR_TYPE_NOT_FOUND _SETUP_PAYLOAD_PARSER_JNI_ERROR(1) -#define SETUP_PAYLOAD_PARSER_JNI_ERROR_METHOD_NOT_FOUND _SETUP_PAYLOAD_PARSER_JNI_ERROR(2) -#define SETUP_PAYLOAD_PARSER_JNI_ERROR_FIELD_NOT_FOUND _SETUP_PAYLOAD_PARSER_JNI_ERROR(3) - -#define JNI_METHOD(RETURN, METHOD_NAME) \ - extern "C" JNIEXPORT RETURN JNICALL Java_chip_onboardingpayload_OnboardingPayloadParser_##METHOD_NAME - -static jobject TransformSetupPayload(JNIEnv * env, SetupPayload & payload); -static jobject CreateCapabilitiesHashSet(JNIEnv * env, RendezvousInformationFlags flags); -static void TransformSetupPayloadFromJobject(JNIEnv * env, jobject jPayload, SetupPayload & payload); -static void CreateCapabilitiesFromHashSet(JNIEnv * env, jobject discoveryCapabilitiesObj, RendezvousInformationFlags & flags); -static CHIP_ERROR ThrowUnrecognizedQRCodeException(JNIEnv * env, jstring qrCodeObj); -static CHIP_ERROR ThrowOnboardingPayloadException(JNIEnv * env, CHIP_ERROR errToThrow); - -jint JNI_OnLoad(JavaVM * jvm, void * reserved) -{ - ChipLogProgress(SetupPayload, "JNI_OnLoad() called"); - chip::Platform::MemoryInit(); - return JNI_VERSION_1_6; -} - -JNI_METHOD(jobject, fetchPayloadFromQrCode)(JNIEnv * env, jobject self, jstring qrCodeObj, jboolean skipPayloadValidation) -{ - CHIP_ERROR err = CHIP_NO_ERROR; - const char * qrString = NULL; - SetupPayload payload; - - qrString = env->GetStringUTFChars(qrCodeObj, 0); - - err = QRCodeSetupPayloadParser(qrString).populatePayload(payload); - env->ReleaseStringUTFChars(qrCodeObj, qrString); - - if (skipPayloadValidation == JNI_FALSE && !payload.isValidQRCodePayload()) - { - ThrowOnboardingPayloadException(env, err); - if (err != CHIP_NO_ERROR) - { - ChipLogError(SetupPayload, "Error throwing OnboardingPayloadException: %" CHIP_ERROR_FORMAT, err.Format()); - } - return nullptr; - } - - if (err != CHIP_NO_ERROR) - { - err = ThrowUnrecognizedQRCodeException(env, qrCodeObj); - if (err != CHIP_NO_ERROR) - { - ChipLogError(SetupPayload, "Error throwing UnrecognizedQRCodeException: %" CHIP_ERROR_FORMAT, err.Format()); - } - return nullptr; - } - - return TransformSetupPayload(env, payload); -} - -jobject TransformSetupPayload(JNIEnv * env, SetupPayload & payload) -{ - jclass setupPayloadClass = env->FindClass("chip/onboardingpayload/OnboardingPayload"); - jmethodID setupConstr = env->GetMethodID(setupPayloadClass, "", "()V"); - jobject setupPayload = env->NewObject(setupPayloadClass, setupConstr); - - jfieldID version = env->GetFieldID(setupPayloadClass, "version", "I"); - jfieldID vendorId = env->GetFieldID(setupPayloadClass, "vendorId", "I"); - jfieldID productId = env->GetFieldID(setupPayloadClass, "productId", "I"); - jfieldID commissioningFlow = env->GetFieldID(setupPayloadClass, "commissioningFlow", "I"); - jfieldID discriminator = env->GetFieldID(setupPayloadClass, "discriminator", "I"); - jfieldID hasShortDiscriminator = env->GetFieldID(setupPayloadClass, "hasShortDiscriminator", "Z"); - jfieldID setUpPinCode = env->GetFieldID(setupPayloadClass, "setupPinCode", "J"); - jfieldID discoveryCapabilities = env->GetFieldID(setupPayloadClass, "discoveryCapabilities", "Ljava/util/Set;"); - - env->SetIntField(setupPayload, version, payload.version); - env->SetIntField(setupPayload, vendorId, payload.vendorID); - env->SetIntField(setupPayload, productId, payload.productID); - env->SetIntField(setupPayload, commissioningFlow, static_cast(payload.commissioningFlow)); - uint16_t discriminatorValue; - bool isShortDiscriminator = payload.discriminator.IsShortDiscriminator(); - if (isShortDiscriminator) - { - discriminatorValue = static_cast(payload.discriminator.GetShortValue()) - << (SetupDiscriminator::kLongBits - SetupDiscriminator::kShortBits); - } - else - { - discriminatorValue = payload.discriminator.GetLongValue(); - } - env->SetIntField(setupPayload, discriminator, discriminatorValue); - env->SetBooleanField(setupPayload, hasShortDiscriminator, isShortDiscriminator); - env->SetLongField(setupPayload, setUpPinCode, payload.setUpPINCode); - - env->SetObjectField(setupPayload, discoveryCapabilities, - CreateCapabilitiesHashSet(env, payload.rendezvousInformation.ValueOr(RendezvousInformationFlag::kNone))); - - jmethodID addOptionalInfoMid = - env->GetMethodID(setupPayloadClass, "addOptionalQRCodeInfo", "(Lchip/onboardingpayload/OptionalQRCodeInfo;)V"); - - std::vector optional_info = payload.getAllOptionalVendorData(); - for (OptionalQRCodeInfo & info : optional_info) - { - - jclass optionalInfoClass = env->FindClass("chip/onboardingpayload/OptionalQRCodeInfo"); - jobject optionalInfo = env->AllocObject(optionalInfoClass); - jfieldID tag = env->GetFieldID(optionalInfoClass, "tag", "I"); - jfieldID type = - env->GetFieldID(optionalInfoClass, "type", "Lchip/onboardingpayload/OptionalQRCodeInfo$OptionalQRCodeInfoType;"); - jfieldID data = env->GetFieldID(optionalInfoClass, "data", "Ljava/lang/String;"); - jfieldID int32 = env->GetFieldID(optionalInfoClass, "int32", "I"); - - env->SetIntField(optionalInfo, tag, info.tag); - - jclass enumClass = env->FindClass("chip/onboardingpayload/OptionalQRCodeInfo$OptionalQRCodeInfoType"); - jfieldID enumType = nullptr; - - switch (info.type) - { - case optionalQRCodeInfoTypeString: - enumType = env->GetStaticFieldID(enumClass, "TYPE_STRING", - "Lchip/onboardingpayload/OptionalQRCodeInfo$OptionalQRCodeInfoType;"); - break; - case optionalQRCodeInfoTypeInt32: - enumType = env->GetStaticFieldID(enumClass, "TYPE_INT32", - "Lchip/onboardingpayload/OptionalQRCodeInfo$OptionalQRCodeInfoType;"); - break; - case optionalQRCodeInfoTypeInt64: - enumType = env->GetStaticFieldID(enumClass, "TYPE_INT64", - "Lchip/onboardingpayload/OptionalQRCodeInfo$OptionalQRCodeInfoType;"); - break; - case optionalQRCodeInfoTypeUInt32: - enumType = env->GetStaticFieldID(enumClass, "TYPE_UINT32", - "Lchip/onboardingpayload/OptionalQRCodeInfo$OptionalQRCodeInfoType;"); - break; - case optionalQRCodeInfoTypeUInt64: - enumType = env->GetStaticFieldID(enumClass, "TYPE_UINT64", - "Lchip/onboardingpayload/OptionalQRCodeInfo$OptionalQRCodeInfoType;"); - break; - case optionalQRCodeInfoTypeUnknown: - default: // Optional Type variable has to set any value. - enumType = env->GetStaticFieldID(enumClass, "TYPE_UNKNOWN", - "Lchip/onboardingpayload/OptionalQRCodeInfo$OptionalQRCodeInfoType;"); - break; - } - - if (enumType != nullptr) - { - jobject enumObj = env->GetStaticObjectField(enumClass, enumType); - env->SetObjectField(optionalInfo, type, enumObj); - } - - env->SetObjectField(optionalInfo, data, env->NewStringUTF(info.data.c_str())); - env->SetIntField(optionalInfo, int32, info.int32); - - env->CallVoidMethod(setupPayload, addOptionalInfoMid, optionalInfo); - } - - return setupPayload; -} - -jobject CreateCapabilitiesHashSet(JNIEnv * env, RendezvousInformationFlags flags) -{ - jclass hashSetClass = env->FindClass("java/util/HashSet"); - jmethodID hashSetConstructor = env->GetMethodID(hashSetClass, "", "()V"); - jobject capabilitiesHashSet = env->NewObject(hashSetClass, hashSetConstructor); - - jmethodID hashSetAddMethod = env->GetMethodID(hashSetClass, "add", "(Ljava/lang/Object;)Z"); - jclass capabilityEnum = env->FindClass("chip/onboardingpayload/DiscoveryCapability"); - - if (flags.Has(chip::RendezvousInformationFlag::kBLE)) - { - jfieldID bleCapability = env->GetStaticFieldID(capabilityEnum, "BLE", "Lchip/onboardingpayload/DiscoveryCapability;"); - jobject enumObj = env->GetStaticObjectField(capabilityEnum, bleCapability); - env->CallBooleanMethod(capabilitiesHashSet, hashSetAddMethod, enumObj); - } - if (flags.Has(chip::RendezvousInformationFlag::kSoftAP)) - { - jfieldID softApCapability = - env->GetStaticFieldID(capabilityEnum, "SOFT_AP", "Lchip/onboardingpayload/DiscoveryCapability;"); - jobject enumObj = env->GetStaticObjectField(capabilityEnum, softApCapability); - env->CallBooleanMethod(capabilitiesHashSet, hashSetAddMethod, enumObj); - } - if (flags.Has(chip::RendezvousInformationFlag::kOnNetwork)) - { - jfieldID onNetworkCapability = - env->GetStaticFieldID(capabilityEnum, "ON_NETWORK", "Lchip/onboardingpayload/DiscoveryCapability;"); - jobject enumObj = env->GetStaticObjectField(capabilityEnum, onNetworkCapability); - env->CallBooleanMethod(capabilitiesHashSet, hashSetAddMethod, enumObj); - } - return capabilitiesHashSet; -} - -JNI_METHOD(jstring, getQrCodeFromPayload)(JNIEnv * env, jobject self, jobject setupPayload) -{ - CHIP_ERROR err = CHIP_NO_ERROR; - SetupPayload payload; - std::string qrString; - - TransformSetupPayloadFromJobject(env, setupPayload, payload); - - err = QRCodeSetupPayloadGenerator(payload).payloadBase38Representation(qrString); - if (err != CHIP_NO_ERROR) - { - ThrowOnboardingPayloadException(env, err); - if (err != CHIP_NO_ERROR) - { - ChipLogError(SetupPayload, "Error throwing OnboardingPayloadException: %" CHIP_ERROR_FORMAT, err.Format()); - } - return nullptr; - } - - return env->NewStringUTF(qrString.c_str()); -} - -void TransformSetupPayloadFromJobject(JNIEnv * env, jobject jPayload, SetupPayload & payload) -{ - jclass setupPayloadClass = env->FindClass("chip/onboardingpayload/OnboardingPayload"); - - jfieldID version = env->GetFieldID(setupPayloadClass, "version", "I"); - jfieldID vendorId = env->GetFieldID(setupPayloadClass, "vendorId", "I"); - jfieldID productId = env->GetFieldID(setupPayloadClass, "productId", "I"); - jfieldID commissioningFlow = env->GetFieldID(setupPayloadClass, "commissioningFlow", "I"); - jfieldID discriminator = env->GetFieldID(setupPayloadClass, "discriminator", "I"); - jfieldID hasShortDiscriminatorFieldId = env->GetFieldID(setupPayloadClass, "hasShortDiscriminator", "Z"); - jfieldID setUpPinCode = env->GetFieldID(setupPayloadClass, "setupPinCode", "J"); - jfieldID discoveryCapabilities = env->GetFieldID(setupPayloadClass, "discoveryCapabilities", "Ljava/util/Set;"); - - payload.version = env->GetIntField(jPayload, version); - payload.vendorID = env->GetIntField(jPayload, vendorId); - payload.productID = env->GetIntField(jPayload, productId); - payload.commissioningFlow = static_cast(env->GetIntField(jPayload, commissioningFlow)); - jboolean hasShortDiscriminator = env->GetBooleanField(jPayload, hasShortDiscriminatorFieldId); - if (hasShortDiscriminator) - { - payload.discriminator.SetShortValue(env->GetShortField(jPayload, discriminator)); - } - else - { - payload.discriminator.SetLongValue(env->GetIntField(jPayload, discriminator)); - } - payload.setUpPINCode = static_cast(env->GetLongField(jPayload, setUpPinCode)); - - jobject discoveryCapabilitiesObj = env->GetObjectField(jPayload, discoveryCapabilities); - CreateCapabilitiesFromHashSet(env, discoveryCapabilitiesObj, - payload.rendezvousInformation.Emplace(RendezvousInformationFlag::kNone)); -} - -void CreateCapabilitiesFromHashSet(JNIEnv * env, jobject discoveryCapabilitiesObj, RendezvousInformationFlags & flags) -{ - jclass hashSetClass = env->FindClass("java/util/HashSet"); - jmethodID hashSetContainsMethod = env->GetMethodID(hashSetClass, "contains", "(Ljava/lang/Object;)Z"); - - jboolean contains; - jclass capabilityEnum = env->FindClass("chip/onboardingpayload/DiscoveryCapability"); - - jfieldID bleCapability = env->GetStaticFieldID(capabilityEnum, "BLE", "Lchip/onboardingpayload/DiscoveryCapability;"); - jobject bleObj = env->GetStaticObjectField(capabilityEnum, bleCapability); - contains = env->CallBooleanMethod(discoveryCapabilitiesObj, hashSetContainsMethod, bleObj); - if (contains) - { - flags.Set(chip::RendezvousInformationFlag::kBLE); - } - - jfieldID softApCapability = env->GetStaticFieldID(capabilityEnum, "SOFT_AP", "Lchip/onboardingpayload/DiscoveryCapability;"); - jobject softApObj = env->GetStaticObjectField(capabilityEnum, softApCapability); - contains = env->CallBooleanMethod(discoveryCapabilitiesObj, hashSetContainsMethod, softApObj); - if (contains) - { - flags.Set(chip::RendezvousInformationFlag::kSoftAP); - } - - jfieldID onNetworkCapability = - env->GetStaticFieldID(capabilityEnum, "ON_NETWORK", "Lchip/onboardingpayload/DiscoveryCapability;"); - jobject onNetworkObj = env->GetStaticObjectField(capabilityEnum, onNetworkCapability); - contains = env->CallBooleanMethod(discoveryCapabilitiesObj, hashSetContainsMethod, onNetworkObj); - if (contains) - { - flags.Set(chip::RendezvousInformationFlag::kOnNetwork); - } -} - -CHIP_ERROR ThrowUnrecognizedQRCodeException(JNIEnv * env, jstring qrCodeObj) -{ - jclass exceptionCls = nullptr; - jmethodID exceptionConstructor = nullptr; - jthrowable exception = nullptr; - - env->ExceptionClear(); - - exceptionCls = env->FindClass("chip/onboardingpayload/UnrecognizedQrCodeException"); - VerifyOrReturnError(exceptionCls != NULL, SETUP_PAYLOAD_PARSER_JNI_ERROR_TYPE_NOT_FOUND); - exceptionConstructor = env->GetMethodID(exceptionCls, "", "(Ljava/lang/String;)V"); - VerifyOrReturnError(exceptionConstructor != NULL, SETUP_PAYLOAD_PARSER_JNI_ERROR_METHOD_NOT_FOUND); - exception = (jthrowable) env->NewObject(exceptionCls, exceptionConstructor, qrCodeObj); - VerifyOrReturnError(exception != NULL, SETUP_PAYLOAD_PARSER_JNI_ERROR_EXCEPTION_THROWN); - - env->Throw(exception); - return CHIP_NO_ERROR; -} - -CHIP_ERROR ThrowOnboardingPayloadException(JNIEnv * env, CHIP_ERROR errToThrow) -{ - jclass exceptionCls = nullptr; - jmethodID exceptionConstructor = nullptr; - jthrowable exception = nullptr; - - env->ExceptionClear(); - - exceptionCls = env->FindClass("chip/onboardingpayload/OnboardingPayloadException"); - VerifyOrReturnError(exceptionCls != NULL, SETUP_PAYLOAD_PARSER_JNI_ERROR_TYPE_NOT_FOUND); - exceptionConstructor = env->GetMethodID(exceptionCls, "", "(Ljava/lang/String;)V"); - VerifyOrReturnError(exceptionConstructor != NULL, SETUP_PAYLOAD_PARSER_JNI_ERROR_METHOD_NOT_FOUND); - - jstring jerrStr = env->NewStringUTF(ErrorStr(errToThrow)); - - exception = (jthrowable) env->NewObject(exceptionCls, exceptionConstructor, jerrStr); - VerifyOrReturnError(exception != NULL, SETUP_PAYLOAD_PARSER_JNI_ERROR_EXCEPTION_THROWN); - - env->Throw(exception); - return CHIP_NO_ERROR; -} diff --git a/src/controller/java/src/chip/onboardingpayload/Base38.kt b/src/controller/java/src/chip/onboardingpayload/Base38.kt new file mode 100644 index 00000000000000..30effe6372dee2 --- /dev/null +++ b/src/controller/java/src/chip/onboardingpayload/Base38.kt @@ -0,0 +1,193 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package chip.onboardingpayload + +private val kCodes = charArrayOf( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', + 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '-', '.' +) +private val kBase38CharactersNeededInNBytesChunk = byteArrayOf(2, 4, 5) +private val kRadix = kCodes.size.toByte() +private val kMaxBytesSingleChunkLen = 3 + +/* + * Implements converting an array of bytes into a Base38 String. + * + * The encoding chosen is: treat every 3 bytes of input data as a little-endian + * uint32_t, then div and mod that into 5 base38 characters, with the least-significant + * encoding bits in the first character of the resulting string. If a number of bytes + * is used that is not multiple of 3, then last 2 bytes are encoded to 4 base38 characters + * or last 1 byte is encoded to 2 base38 characters. Algoritm considers worst case size + * of bytes chunks and does not introduce code length optimization. + * + * The resulting size of the out_buf span will be the size of data written. + */ +fun base38Encode(inBuf: ByteArray, outBuf: CharArray): Unit { + var inBufLen = inBuf.size + var inIdx = 0 + var outIdx = 0 + + while (inBufLen > 0) { + var value = 0 + val bytesInChunk = if (inBufLen >= kMaxBytesSingleChunkLen) kMaxBytesSingleChunkLen else inBufLen + + for (byteIdx in 0 until bytesInChunk) { + value += (inBuf[inIdx + byteIdx].toInt() and 0xFF) shl (8 * byteIdx) + } + inBufLen -= bytesInChunk + inIdx += bytesInChunk + + val base38CharactersNeeded = kBase38CharactersNeededInNBytesChunk[bytesInChunk - 1].toByte() + + if ((outIdx + base38CharactersNeeded) >= outBuf.size) { + throw OnboardingPayloadException("Buffer is too small") + } + + for (character in 0 until base38CharactersNeeded) { + outBuf[outIdx++] = kCodes[value % kRadix] + value /= kRadix.toInt() + } + } + + if (outIdx < outBuf.size) { + outBuf[outIdx] = '\u0000' + } else { + throw OnboardingPayloadException("Buffer is too small") + } +} + +/* + * Returns size needed to store encoded string given number of input bytes. + */ +fun base38EncodedLength(numBytes: Int): Int { + return (numBytes / 3) * 5 + (numBytes % 3) * 2 +} + +/** + * Implements converting a Base38 String into an array of bytes. + * + */ +fun base38Decode(base38: String): ArrayList { + val result = ArrayList() + var base38CharactersNumber = base38.length + var decodedBase38Characters = 0 + + while (base38CharactersNumber > 0) { + val base38CharactersInChunk: Byte + val bytesInDecodedChunk: Byte + + if (base38CharactersNumber >= kBase38CharactersNeededInNBytesChunk[2]) { + base38CharactersInChunk = kBase38CharactersNeededInNBytesChunk[2] + bytesInDecodedChunk = 3 + } else if (base38CharactersNumber == kBase38CharactersNeededInNBytesChunk[1].toInt()) { + base38CharactersInChunk = kBase38CharactersNeededInNBytesChunk[1] + bytesInDecodedChunk = 2 + } else if (base38CharactersNumber == kBase38CharactersNeededInNBytesChunk[0].toInt()) { + base38CharactersInChunk = kBase38CharactersNeededInNBytesChunk[0] + bytesInDecodedChunk = 1 + } else { + throw OnboardingPayloadException("Invalid string length") + } + + var value = 0 + for (i in base38CharactersInChunk downTo 1) { + val v = decodeChar(base38[decodedBase38Characters + i - 1]) + if (v < 0) { + throw OnboardingPayloadException("Invalid integer value") + } + value = value * kRadix + v + } + + decodedBase38Characters += base38CharactersInChunk + base38CharactersNumber -= base38CharactersInChunk.toInt() + + for (i in 0 until bytesInDecodedChunk) { + result.add(value.toByte()) + value = value shr 8 + } + + if (value > 0) { + throw OnboardingPayloadException("Invalid argument") + } + } + + return result +} + +private fun decodeChar(c: Char): Byte { + val kBogus: Byte = -1 + val decodes = byteArrayOf( + 36, // '-', =45 + 37, // '.', =46 + kBogus, // '/', =47 + 0, // '0', =48 + 1, // '1', =49 + 2, // '2', =50 + 3, // '3', =51 + 4, // '4', =52 + 5, // '5', =53 + 6, // '6', =54 + 7, // '7', =55 + 8, // '8', =56 + 9, // '9', =57 + kBogus, // ':', =58 + kBogus, // ';', =59 + kBogus, // '<', =50 + kBogus, // '=', =61 + kBogus, // '>', =62 + kBogus, // '?', =63 + kBogus, // '@', =64 + 10, // 'A', =65 + 11, // 'B', =66 + 12, // 'C', =67 + 13, // 'D', =68 + 14, // 'E', =69 + 15, // 'F', =70 + 16, // 'G', =71 + 17, // 'H', =72 + 18, // 'I', =73 + 19, // 'J', =74 + 20, // 'K', =75 + 21, // 'L', =76 + 22, // 'M', =77 + 23, // 'N', =78 + 24, // 'O', =79 + 25, // 'P', =80 + 26, // 'Q', =81 + 27, // 'R', =82 + 28, // 'S', =83 + 29, // 'T', =84 + 30, // 'U', =85 + 31, // 'V', =86 + 32, // 'W', =87 + 33, // 'X', =88 + 34, // 'Y', =89 + 35 // 'Z', =90 + ) + + if (c < '-' || c > 'Z') { + throw OnboardingPayloadException("Invalid character: $c") + } + + val v: Byte = decodes[c - '-'] + if (v == kBogus) { + throw OnboardingPayloadException("Invalid integer value") + } + + return v +} diff --git a/src/controller/java/src/chip/onboardingpayload/OnboardingPayload.kt b/src/controller/java/src/chip/onboardingpayload/OnboardingPayload.kt index 23b9b2fa35b3b0..af46eb5c26dcf7 100644 --- a/src/controller/java/src/chip/onboardingpayload/OnboardingPayload.kt +++ b/src/controller/java/src/chip/onboardingpayload/OnboardingPayload.kt @@ -95,7 +95,7 @@ class OnboardingPayload( /** * The CHIP device supported rendezvous flags: At least one DiscoveryCapability must be included. */ - var discoveryCapabilities: Set = emptySet(), + var discoveryCapabilities: MutableSet = mutableSetOf(), /** The CHIP device discriminator: */ var discriminator: Int = 0, @@ -115,9 +115,13 @@ class OnboardingPayload( var setupPinCode: Long = 0 ) { var optionalQRCodeInfo: HashMap + private val optionalVendorData: HashMap + private val optionalExtensionData: HashMap init { optionalQRCodeInfo = HashMap() + optionalVendorData = HashMap() + optionalExtensionData = HashMap() } constructor( @@ -125,7 +129,7 @@ class OnboardingPayload( vendorId: Int, productId: Int, commissioningFlow: Int, - discoveryCapabilities: Set, + discoveryCapabilities: MutableSet, discriminator: Int, setupPinCode: Long ) : this( @@ -151,6 +155,46 @@ class OnboardingPayload( return checkPayloadCommonConstraints() } + fun isValidQRCodePayload(): Boolean { + // 3-bit value specifying the QR code payload version. + if (version >= 1 shl kVersionFieldLengthInBits) { + return false + } + + if (commissioningFlow.toUInt() > ((1 shl kCommissioningFlowFieldLengthInBits) - 1).toUInt()) { + return false + } + + // Device Commissioning Flow + // 0: Standard commissioning flow: such a device, when uncommissioned, always enters commissioning mode upon power-up, subject + // to the rules in [ref_Announcement_Commencement]. 1: User-intent commissioning flow: user action required to enter + // commissioning mode. 2: Custom commissioning flow: interaction with a vendor-specified means is needed before commissioning. + // 3: Reserved + if (commissioningFlow != CommissioningFlow.STANDARD.value && + commissioningFlow != CommissioningFlow.USER_ACTION_REQUIRED.value && + commissioningFlow != CommissioningFlow.CUSTOM.value) { + return false + } + + val allValid = setOf( + DiscoveryCapability.BLE, + DiscoveryCapability.ON_NETWORK, + DiscoveryCapability.SOFT_AP + ) + + // If discoveryCapabilities is empty or discoveryCapabilities contains values outside of allValid + if (discoveryCapabilities.isEmpty() || discoveryCapabilities.any { it !in allValid }) { + return false + } + + // Discriminator validity is enforced by the SetupDiscriminator class. + if (setupPinCode >= 1 shl kSetupPINCodeFieldLengthInBits) { + return false + } + + return checkPayloadCommonConstraints() + } + fun getShortDiscriminatorValue(): Int { if (hasShortDiscriminator) { return discriminator @@ -165,6 +209,266 @@ class OnboardingPayload( return discriminator } + fun getRendezvousInformation(): Long { + var rendezvousInfo: Long = 0 + + if (discoveryCapabilities.contains(DiscoveryCapability.SOFT_AP)) { + // set bit 0 + rendezvousInfo = rendezvousInfo or (1L shl 0) + } + + if (discoveryCapabilities.contains(DiscoveryCapability.BLE)) { + // set bit 1 + rendezvousInfo = rendezvousInfo or (1L shl 1) + } + + if (discoveryCapabilities.contains(DiscoveryCapability.ON_NETWORK)) { + // set bit 2 + rendezvousInfo = rendezvousInfo or (1L shl 2) + } + + return rendezvousInfo + } + + fun setRendezvousInformation(rendezvousInfo: Long) { + // Removes all elements from discoveryCapabilities. + discoveryCapabilities.clear() + + // bit 0 is set + if (rendezvousInfo and (1L shl 0) != 0L) { + discoveryCapabilities.add(DiscoveryCapability.SOFT_AP) + } + + // bit 1 is set + if (rendezvousInfo and (1L shl 1) != 0L) { + discoveryCapabilities.add(DiscoveryCapability.BLE) + } + + // bit 2 is set + if (rendezvousInfo and (1L shl 2) != 0L) { + discoveryCapabilities.add(DiscoveryCapability.ON_NETWORK) + } + } + + /** + * A function to add a String serial number + * + * @param serialNumber String serial number + */ + fun addSerialNumber(serialNumber: String) { + val info = OptionalQRCodeInfoExtension() + info.tag = kSerialNumberTag + info.type = OptionalQRCodeInfoType.TYPE_STRING + info.data = serialNumber + + addOptionalExtensionData(info) + } + + /** + * A function to add a Int serial number + * + * @param serialNumber Int serial number + */ + fun addSerialNumber(serialNumber: Int) { + val info = OptionalQRCodeInfoExtension() + info.tag = kSerialNumberTag + info.type = OptionalQRCodeInfoType.TYPE_UINT32 + info.uint32 = serialNumber.toLong() + + addOptionalExtensionData(info) + } + + /** + * A function to retrieve serial number as a string + * + * @return retrieved string serial number + */ + fun getSerialNumber(): String { + val outSerialNumber = StringBuilder() + val info = getOptionalExtensionData(kSerialNumberTag) + + when (info.type) { + OptionalQRCodeInfoType.TYPE_STRING -> outSerialNumber.append(info.data) + OptionalQRCodeInfoType.TYPE_UINT32 -> outSerialNumber.append(info.uint32) + else -> throw OnboardingPayloadException("Invalid argument") + } + + return outSerialNumber.toString() + } + + /** + * A function to remove the serial number from the payload + */ + fun removeSerialNumber() { + if (optionalExtensionData.containsKey(kSerialNumberTag)) { + optionalExtensionData.remove(kSerialNumberTag) + return + } else { + throw OnboardingPayloadException("Key not found") + } + } + + /** + * Checks if the tag is CHIP Common type + * Spec 5.1.4.2 CHIPCommon tag numbers are in the range [0x00, 0x7F] + * + * @param tag Tag to be checked + * @return True if the tag is of Common type, False otherwise + */ + private fun isCommonTag(tag: Int): Boolean { + return tag < 0x80 + } + + /** + * Checks if the tag is vendor-specific + * Spec 5.1.4.1 Manufacture-specific tag numbers are in the range [0x80, 0xFF] + * + * @param tag Tag to be checked + * @return True if the tag is Vendor-specific, False otherwise + */ + private fun isVendorTag(tag: Int): Boolean { + return !isCommonTag(tag) + } + + /** + * A function to add an optional vendor data + * + * @param tag tag number in the [0x80-0xFF] range + * @param data String representation of data to add + */ + fun addOptionalVendorData(tag: Int, data: String) { + val info = OptionalQRCodeInfo() + info.tag = tag + info.type = OptionalQRCodeInfoType.TYPE_STRING + info.data = data + + addOptionalVendorData(info) + } + + /** + * A function to add an optional vendor data + * + * @param tag 7 bit [0-127] tag number + * @param data Integer representation of data to add + */ + fun addOptionalVendorData(tag: Int, data: Int) { + val info = OptionalQRCodeInfo() + info.tag = tag + info.type = OptionalQRCodeInfoType.TYPE_INT32 + info.int32 = data + + addOptionalVendorData(info) + } + + /** + * A function to add an optional QR Code info vendor object + * + * @param info Optional QR code info object to add + */ + private fun addOptionalVendorData(info: OptionalQRCodeInfo) { + if (isVendorTag(info.tag)) { + optionalVendorData[info.tag] = info + return + } else { + throw OnboardingPayloadException("Invalid argument") + } + } + + /** + * A function to remove an optional vendor data + * + * @param tag 7 bit [0-127] tag number + */ + fun removeOptionalVendorData(tag: Int) { + if (optionalVendorData.containsKey(tag)) { + optionalVendorData.remove(tag) + return + } else { + throw OnboardingPayloadException("Key not found") + } + } + + /** + * A function to retrieve the vector of OptionalQRCodeInfo infos + * + * @return a vector of optionalQRCodeInfos + */ + fun getAllOptionalVendorData(): List { + val returnedOptionalInfo = mutableListOf() + + for (entry in optionalVendorData) { + returnedOptionalInfo.add(entry.value) + } + + return returnedOptionalInfo + } + + /** + * A function to add an optional QR Code info CHIP object + * + * @param info Optional QR code info object to add + */ + private fun addOptionalExtensionData(info: OptionalQRCodeInfoExtension) { + if (isCommonTag(info.tag)) { + optionalExtensionData[info.tag] = info + return + } else { + throw OnboardingPayloadException("Invalid argument") + } + } + + /** + * A function to retrieve the vector of CHIPQRCodeInfo infos + * + * @return a vector of OptionalQRCodeInfoExtension + */ + fun getAllOptionalExtensionData(): List { + val returnedOptionalInfo = mutableListOf() + for (entry in optionalExtensionData) { + returnedOptionalInfo.add(entry.value) + } + + return returnedOptionalInfo + } + + /** + * A function to retrieve an optional QR Code info vendor object + * + * @param tag 7 bit [0-127] tag number + * @return retrieved OptionalQRCodeInfo object + */ + private fun getOptionalVendorData(tag: Int): OptionalQRCodeInfo { + return optionalVendorData[tag] ?: throw OnboardingPayloadException("Key not found") + } + + /** + * A function to retrieve an optional QR Code info extended object + * + * @param tag 8 bit [128-255] tag number + * @return retrieved OptionalQRCodeInfoExtension object + */ + private fun getOptionalExtensionData(tag: Int): OptionalQRCodeInfoExtension { + return optionalExtensionData[tag] ?: throw OnboardingPayloadException("Key not found") + } + + /** + * A function to retrieve the associated expected numeric value for a tag + * + * @param tag 8 bit [0-255] tag number + * @return an OptionalQRCodeInfoType value + */ + private fun getNumericTypeFor(tag: Int): OptionalQRCodeInfoType { + var elemType = OptionalQRCodeInfoType.TYPE_UNKNOWN + + if (isVendorTag(tag)) { + elemType = OptionalQRCodeInfoType.TYPE_INT32 + } else if (tag == kSerialNumberTag) { + elemType = OptionalQRCodeInfoType.TYPE_UINT32 + } + + return elemType + } + private fun checkPayloadCommonConstraints(): Boolean { if (version != 0) { return false diff --git a/src/controller/java/src/chip/onboardingpayload/OnboardingPayloadParser.kt b/src/controller/java/src/chip/onboardingpayload/OnboardingPayloadParser.kt index 8e428639fc7d9f..ec0f1b999a017c 100644 --- a/src/controller/java/src/chip/onboardingpayload/OnboardingPayloadParser.kt +++ b/src/controller/java/src/chip/onboardingpayload/OnboardingPayloadParser.kt @@ -48,12 +48,25 @@ class OnboardingPayloadParser { /** Get QR code string from [OnboardingPayload]. */ @Throws(OnboardingPayloadException::class) - external fun getQrCodeFromPayload(payload: OnboardingPayload): String + fun getQrCodeFromPayload(payload: OnboardingPayload): String + { + return QRCodeOnboardingPayloadGenerator(payload).payloadBase38Representation(); + } @Throws(UnrecognizedQrCodeException::class, OnboardingPayloadException::class) - private external fun fetchPayloadFromQrCode( + private fun fetchPayloadFromQrCode( qrCodeString: String, skipPayloadValidation: Boolean - ): OnboardingPayload + ): OnboardingPayload { + val payload = OnboardingPayload() + + QRCodeOnboardingPayloadParser(qrCodeString).populatePayload(payload) + + if (skipPayloadValidation == false && !payload.isValidQRCodePayload()) { + throw OnboardingPayloadException("Invalid payload") + } + + return payload + } /** Get Manual Pairing Code string from [OnboardingPayload]. */ @Throws(OnboardingPayloadException::class) diff --git a/src/controller/java/src/chip/onboardingpayload/OptionalQRCodeInfo.kt b/src/controller/java/src/chip/onboardingpayload/OptionalQRCodeInfo.kt index 8f5a02ed631db5..ebbb504d5f1764 100644 --- a/src/controller/java/src/chip/onboardingpayload/OptionalQRCodeInfo.kt +++ b/src/controller/java/src/chip/onboardingpayload/OptionalQRCodeInfo.kt @@ -26,9 +26,16 @@ enum class OptionalQRCodeInfoType { TYPE_UINT64 } -data class OptionalQRCodeInfo( - var tag: Int = 0, - var type: OptionalQRCodeInfoType = OptionalQRCodeInfoType.TYPE_UNKNOWN, - var data: String? = null, +open class OptionalQRCodeInfo { + var tag: Int = 0 + var type: OptionalQRCodeInfoType = OptionalQRCodeInfoType.TYPE_UNKNOWN + var data: String? = null var int32: Int = 0 -) +} + +class OptionalQRCodeInfoExtension : OptionalQRCodeInfo() { + var int64: Long = 0 + var uint32: Long = 0 + var uint64: Long = 0 +} + diff --git a/src/controller/java/src/chip/onboardingpayload/QRCodeBasicOnboardingPayloadGenerator.kt b/src/controller/java/src/chip/onboardingpayload/QRCodeBasicOnboardingPayloadGenerator.kt new file mode 100644 index 00000000000000..8429e4b0388689 --- /dev/null +++ b/src/controller/java/src/chip/onboardingpayload/QRCodeBasicOnboardingPayloadGenerator.kt @@ -0,0 +1,145 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package chip.onboardingpayload + +import java.lang.StringBuilder + +/** + * A minimal QR code setup payload generator that omits any optional data, + * for compatibility with devices that don't support std::string or STL. + */ +class QRCodeBasicOnboardingPayloadGenerator(private val payload: OnboardingPayload) { + + /** + * This function is called to encode the binary data of a payload to a + * base38 null-terminated string. + * + * The resulting size of the outBuffer span will be the size of data written. + * + * This function will fail if the payload has any optional data requiring + * TLV encoding. + * + * @param[out] outBuffer The buffer to copy the base38 to. + * + */ + fun payloadBase38Representation(outBuffer: CharArray): Unit { + val bits = ByteArray(kTotalPayloadDataSizeInBytes) + if (!payload.isValidQRCodePayload()) { + throw OnboardingPayloadException("Invalid argument") + } + + payloadBase38RepresentationWithTLV(payload, outBuffer, bits, null, 0) + } +} + +fun payloadBase38RepresentationWithTLV( + payload: OnboardingPayload, + outBuffer: CharArray, + bits: ByteArray, + tlvDataStart: ByteArray?, + tlvDataLengthInBytes: Int +) { + bits.fill(0) + generateBitSet(payload, bits, tlvDataStart, tlvDataLengthInBytes) + + val prefixLen = kQRCodePrefix.length + + if (outBuffer.size < prefixLen) { + throw OnboardingPayloadException("Buffer is too small") + } else { + val subBuffer = outBuffer.copyOfRange(prefixLen, outBuffer.size) + kQRCodePrefix.toCharArray(outBuffer, 0, prefixLen) + + base38Encode(bits, subBuffer) + + // Copy the subBuffer back to the outBuffer + subBuffer.copyInto(outBuffer, prefixLen) + + // Reduce output buffer size to be the size of written data + outBuffer.copyOf(prefixLen + subBuffer.size) + } +} + +private fun generateBitSet( + payload: OnboardingPayload, + bits: ByteArray, + tlvDataStart: ByteArray?, + tlvDataLengthInBytes: Int +) { + var offset = 0 + val totalPayloadSizeInBits = kTotalPayloadDataSizeInBits + (tlvDataLengthInBytes * 8) + if (bits.size * 8 < totalPayloadSizeInBits) + throw OnboardingPayloadException("Buffer is too small") + + populateBits(bits, offset, payload.version.toLong(), kVersionFieldLengthInBits, kTotalPayloadDataSizeInBits) + populateBits(bits, offset, payload.vendorId.toLong(), kVendorIDFieldLengthInBits, kTotalPayloadDataSizeInBits) + populateBits(bits, offset, payload.productId.toLong(), kProductIDFieldLengthInBits, kTotalPayloadDataSizeInBits) + populateBits(bits, offset, payload.commissioningFlow.toLong(), kCommissioningFlowFieldLengthInBits, kTotalPayloadDataSizeInBits) + + if (payload.discoveryCapabilities.isEmpty()) + throw OnboardingPayloadException("Invalid argument") + + populateBits(bits, offset, payload.getRendezvousInformation(), kRendezvousInfoFieldLengthInBits, kTotalPayloadDataSizeInBits) + populateBits(bits, offset, payload.discriminator.toLong(), kPayloadDiscriminatorFieldLengthInBits, kTotalPayloadDataSizeInBits) + populateBits(bits, offset, payload.setupPinCode, kSetupPINCodeFieldLengthInBits, kTotalPayloadDataSizeInBits) + populateBits(bits, offset, 0L, kPaddingFieldLengthInBits, kTotalPayloadDataSizeInBits) + populateTLVBits(bits, offset, tlvDataStart, tlvDataLengthInBytes, totalPayloadSizeInBits) +} + +// Populates numberOfBits starting from LSB of input into bits, which is assumed to be zero-initialized +private fun populateBits( + bits: ByteArray, + offset: Int, + input: Long, + numberOfBits: Int, + totalPayloadDataSizeInBits: Int +) { + if (offset + numberOfBits > totalPayloadDataSizeInBits) + throw OnboardingPayloadException("Invalid argument") + + if (input >= (1L shl numberOfBits)) + throw OnboardingPayloadException("Invalid argument") + + var index = offset + var inputValue = input + while (inputValue != 0L) { + if (inputValue and 1L != 0L) { + val mask = 1 shl (index % 8) + bits[index / 8] = (bits[index / 8].toInt() or mask).toByte() + } + index++ + inputValue = inputValue shr 1 + } +} + +private fun populateTLVBits( + bits: ByteArray, + offset: Int, + tlvBuf: ByteArray?, + tlvBufSizeInBytes: Int, + totalPayloadDataSizeInBits: Int +) { + if (tlvBuf == null) { + return + } + + for (i in 0 until tlvBufSizeInBytes) { + val value = tlvBuf[i] + populateBits(bits, offset, value.toLong(), 8, totalPayloadDataSizeInBits) + } +} diff --git a/src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadGenerator.kt b/src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadGenerator.kt new file mode 100644 index 00000000000000..db8a10d36b2c07 --- /dev/null +++ b/src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadGenerator.kt @@ -0,0 +1,186 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package chip.onboardingpayload + +import java.lang.StringBuilder +import chip.tlv.Tag +import chip.tlv.AnonymousTag +import chip.tlv.ContextSpecificTag +import chip.tlv.TlvWriter + +class QRCodeOnboardingPayloadGenerator(private val onboardingPayload: OnboardingPayload) { + private var allowInvalidPayload = false + + /** + * This function is called to encode the binary data of a payload to a + * base38 string. + * + * If the payload has any optional data that needs to be TLV encoded, this + * function will fail. + * + * @retval The base38 representation string. + */ + fun payloadBase38Representation(): String { + return payloadBase38Representation(null, 0) + } + + /** + * This function is called to encode the binary data of a payload to a + * base38 null-terminated string. + * + * If the payload has any optional data that needs to be TLV encoded, this + * function will allocate a scratch heap buffer to hold the TLV data while + * encoding. + * + * @retval The base38 representation string. + */ + fun payloadBase38RepresentationWithAutoTLVBuffer(): String { + // Estimate the size of the needed buffer. + var estimate = 0 + + val dataItemSizeEstimate: (OptionalQRCodeInfo) -> Int = { item -> + // Each data item needs a control byte and a context tag. + var size = 2 + + if (item.type == OptionalQRCodeInfoType.TYPE_STRING) { + // We'll need to encode the string length and then the string data. + // Length is at most 8 bytes. + size += 8 + size += item.data?.length ?: 0 + } else { + // Integer. Assume it might need up to 8 bytes, for simplicity. + size += 8 + } + size + } + + val vendorData = onboardingPayload.getAllOptionalVendorData() + for (data in vendorData) { + estimate += dataItemSizeEstimate(data) + } + + val extensionData = onboardingPayload.getAllOptionalExtensionData() + for (data in extensionData) { + estimate += dataItemSizeEstimate(data) + } + + estimate = estimateStructOverhead(estimate) + + val tlvDataStart: ByteArray? = ByteArray(estimate) + + return payloadBase38Representation(tlvDataStart, estimate) + } + + fun setAllowInvalidPayload(allow: Boolean) { + allowInvalidPayload = allow + } + + /** + * This function is called to encode the binary data of a payload to a + * base38 string, using the caller-provided buffer as + * temporary scratch space for optional data that needs to be TLV-encoded. + * If that buffer is not big enough to hold the TLV-encoded part of the + * payload, the function will fail. + * @param[in] tlvDataStart + * The start of the buffer to use as temporary scratch space + * for optional data that needs to be TLV-encoded. + * + * @param[in] tlvDataStartSize + * The size of the buffer. + * + * @retval The base38 representation string. + */ + private fun payloadBase38Representation(tlvDataStart: ByteArray?, tlvDataStartSize: Int): String { + if (!allowInvalidPayload && !onboardingPayload.isValidQRCodePayload()) { + throw OnboardingPayloadException("Invalid argument") + } + + var tlvDataLengthInBytes = generateTLVFromOptionalData(onboardingPayload, tlvDataStart, tlvDataStartSize) + + val bits = ByteArray(kTotalPayloadDataSizeInBytes + tlvDataLengthInBytes) + val buffer = CharArray(base38EncodedLength(bits.size) + kQRCodePrefix.length) + payloadBase38RepresentationWithTLV(onboardingPayload, buffer, bits, tlvDataStart, tlvDataLengthInBytes) + + return buffer.toString() + } + + private fun generateTLVFromOptionalData( + outPayload: OnboardingPayload, + tlvDataStart: ByteArray?, + maxLen: Int + ): Int { + val optionalData = outPayload.getAllOptionalVendorData() + val optionalExtensionData = outPayload.getAllOptionalExtensionData() + if (optionalData.isEmpty() && optionalExtensionData.isEmpty()) { + return 0 + } + + val rootWriter = TlvWriter(maxLen) + val innerStructureWriter = rootWriter.startStructure(AnonymousTag) + + for (info in optionalData) { + writeTag(innerStructureWriter, ContextSpecificTag(info.tag), info) + } + + for (info in optionalExtensionData) { + writeTag(innerStructureWriter, ContextSpecificTag(info.tag), info) + } + + rootWriter.endStructure() + rootWriter.validateTlv() + + val tlvDataLengthInBytes = rootWriter.getLengthWritten() + val encodedTlvData = rootWriter.getEncoded() + + if (tlvDataStart != null) { + System.arraycopy(encodedTlvData, 0, tlvDataStart, 0, tlvDataLengthInBytes) + } + + return tlvDataLengthInBytes + } + + private fun writeTag(writer: TlvWriter, tag: Tag, info: OptionalQRCodeInfo) { + when (info.type) { + OptionalQRCodeInfoType.TYPE_STRING -> writer.put(tag, info.data!!) + OptionalQRCodeInfoType.TYPE_INT32 -> writer.put(tag, info.int32) + else -> throw OnboardingPayloadException("Invalid argument") + } + } + + private fun writeTag(writer: TlvWriter, tag: Tag, info: OptionalQRCodeInfoExtension) { + when (info.type) { + OptionalQRCodeInfoType.TYPE_STRING -> writeTag(writer, tag, info as OptionalQRCodeInfo) + OptionalQRCodeInfoType.TYPE_INT64 -> writer.put(tag, info.int64) + OptionalQRCodeInfoType.TYPE_UINT32 -> writer.put(tag, info.uint32) + OptionalQRCodeInfoType.TYPE_UINT64 -> writer.put(tag, info.uint64) + else -> throw OnboardingPayloadException("Invalid argument") + } + } + + private fun estimateStructOverhead(): Int { + // The struct itself has a control byte and an end-of-struct marker. + return 2 + } + + private fun estimateStructOverhead(firstFieldSize: Int): Int { + // Estimate 4 bytes of overhead per field. This can happen for a large + // octet string field: 1 byte control, 1 byte context tag, 2 bytes + // length. + return firstFieldSize + 4 + estimateStructOverhead() + } +} diff --git a/src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadParser.kt b/src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadParser.kt new file mode 100644 index 00000000000000..0dd34e6765b725 --- /dev/null +++ b/src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadParser.kt @@ -0,0 +1,122 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package chip.onboardingpayload + +import java.lang.StringBuilder +import java.util.concurrent.atomic.AtomicInteger +import chip.tlv.Tag +import chip.tlv.AnonymousTag +import chip.tlv.ContextSpecificTag +import chip.tlv.TlvReader + +/** + * @class QRCodeOnboardingPayloadParser + * A class that can be used to convert a base38 encoded payload to a OnboardingPayload object + */ +class QRCodeOnboardingPayloadParser(private val mBase38Representation: String) { + private fun extractPayload(inString: String): String { + var chipSegment = "" + val delimiter = '%' + val startIndices = mutableListOf() + startIndices.add(0) + + for (i in inString.indices) { + if (inString[i] == delimiter) { + startIndices.add(i + 1) + } + } + + // Find the first string between delimiters that starts with kQRCodePrefix + for (i in 0 until startIndices.size) { + val startIndex = startIndices[i] + val endIndex = if (i == startIndices.size - 1) inString.length else startIndices[i + 1] - 1 + val length = if (endIndex != inString.length) endIndex - startIndex else inString.length + val segment = inString.substring(startIndex, startIndex + length) + + // Find a segment that starts with kQRCodePrefix + if (segment.startsWith(kQRCodePrefix) && segment.length > kQRCodePrefix.length) { + chipSegment = segment + break + } + } + + if (chipSegment.length > 0) { + return chipSegment.substring(kQRCodePrefix.length) // strip out prefix before returning + } + + return chipSegment + } + + fun populatePayload(outPayload: OnboardingPayload) { + var indexToReadFrom: AtomicInteger = AtomicInteger(0) + + val payload = extractPayload(mBase38Representation) + if (payload.length == 0) { + throw UnrecognizedQrCodeException("Invalid argument") + } + + var buf = base38Decode(payload) + var dest = readBits(buf, indexToReadFrom, kVersionFieldLengthInBits) + outPayload.version = dest.toInt() + + dest = readBits(buf, indexToReadFrom, kVendorIDFieldLengthInBits) + outPayload.vendorId = dest.toInt() + + dest = readBits(buf, indexToReadFrom, kProductIDFieldLengthInBits) + outPayload.productId = dest.toInt() + + dest = readBits(buf, indexToReadFrom, kCommissioningFlowFieldLengthInBits) + outPayload.commissioningFlow = dest.toInt() + + dest = readBits(buf, indexToReadFrom, kRendezvousInfoFieldLengthInBits) + outPayload.setRendezvousInformation(dest) + + dest = readBits(buf, indexToReadFrom, kPayloadDiscriminatorFieldLengthInBits) + outPayload.discriminator = dest.toInt() + + dest = readBits(buf, indexToReadFrom, kSetupPINCodeFieldLengthInBits) + outPayload.setupPinCode = dest + + dest = readBits(buf, indexToReadFrom, kPaddingFieldLengthInBits) + if (dest != 0L) { + throw UnrecognizedQrCodeException("Invalid argument") + } + + // TODO: populate TLV optional fields + } + + companion object { + // Populate numberOfBits into dest from buf starting at startIndex + fun readBits(buf: ArrayList, index: AtomicInteger, numberOfBitsToRead: Int): Long { + var dest: Long = 0 + if (index.get() + numberOfBitsToRead > buf.size * 8 || numberOfBitsToRead > Long.SIZE_BITS) { + throw UnrecognizedQrCodeException("Invalid argument") + } + + var currentIndex = index.get() + for (bitsRead in 0 until numberOfBitsToRead) { + if (buf[currentIndex / 8].toInt() and (1 shl (currentIndex % 8)) != 0) { + dest = dest or (1L shl bitsRead) + } + currentIndex++ + } + index.addAndGet(numberOfBitsToRead) + return dest + } + } +}