Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ option(ENABLE_MQTT "MQTT broker support" ON)
option(ENABLE_OPENSSL "OpenSSL for various encryption needs" ON)
option(ENABLE_MBEDTLS "mbedTLS for various encryption needs" ON)
option(ENABLE_SECURE_STORAGE "Store MQTT/MySQL passwords in the OS secure store via QtKeychain" ON)
option(ENABLE_TESTS "Build the Entitlement unit tests (developer-only)" OFF)

################################################################################

Expand Down Expand Up @@ -116,6 +117,12 @@ set(SOURCES
src/rc4/rc4.cpp src/rc4/rc4.h
src/blufi/BluFiFrame.cpp src/blufi/BluFiFrame.h src/blufi/BluFiUtils.h

src/Entitlement/Entitlement.cpp src/Entitlement/Entitlement.h
src/Entitlement/EntitlementManager.cpp src/Entitlement/EntitlementManager.h
src/Entitlement/EntitlementEras.cpp src/Entitlement/EntitlementEras.h
src/Entitlement/Provenance.cpp src/Entitlement/Provenance.h
src/Entitlement/ProvenanceStore.cpp src/Entitlement/ProvenanceStore.h

thirdparty/TheengsDecoder/src/decoder.cpp thirdparty/TheengsDecoder/src/decoder.h
thirdparty/IconLibrary/IconLibrary_material.qrc

Expand Down Expand Up @@ -584,3 +591,12 @@ set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
#)

################################################################################

# Developer-only unit tests for the Entitlement module. Off by default so the
# shipped app build is unaffected. Enable with: cmake -B build/ -DENABLE_TESTS=ON
if(ENABLE_TESTS)
enable_testing()
add_subdirectory(tests)
endif()

################################################################################
5 changes: 4 additions & 1 deletion assets/android/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@
<application android:name="org.qtproject.qt.android.bindings.QtApplication"
android:hardwareAccelerated="true" android:allowNativeHeapPointerTagging="false"
android:theme="@style/AppTheme" android:roundIcon="@mipmap/ic_launcher_round" android:icon="@mipmap/ic_launcher"
android:label="Theengs BLE">
android:label="Theengs BLE"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/data_extraction_rules">

<!-- Activity -->
<activity android:name="org.qtproject.qt.android.bindings.QtActivity"
Expand Down
9 changes: 9 additions & 0 deletions assets/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ android {
targetCompatibility JavaVersion.VERSION_17
}

// AGP 8+ defaults this to false. The Entitlement module's startup stamp
// reads BuildConfig.VERSION_CODE via JNI to record firstSeenBuild in the
// provenance object (see src/Entitlement/Entitlement.cpp); without this
// flag the BuildConfig class is never generated and the JNI lookup fails
// silently, recording build=0.
buildFeatures {
buildConfig true
}

packagingOptions {
jniLibs {
useLegacyPackaging true
Expand Down
11 changes: 11 additions & 0 deletions assets/android/res/xml/backup_rules.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
android:fullBackupContent for API < 31. Includes the QSettings file
under <files>/settings/Theengs/, which holds the Release A entitlement
provenance stamp (see src/Entitlement/). Excludes the cache directory
to avoid restoring stale device state.
-->
<full-backup-content>
<include domain="file" path="settings/"/>
<exclude domain="file" path="cache/"/>
</full-backup-content>
17 changes: 17 additions & 0 deletions assets/android/res/xml/data_extraction_rules.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
android:dataExtractionRules for API 31+ (Android 12). Same intent as
backup_rules: keep the QSettings settings/ subtree in both cloud backup
and device-to-device transfer so the Release A entitlement provenance
stamp follows the user to a new device on the same Google account.
-->
<data-extraction-rules>
<cloud-backup>
<include domain="file" path="settings/"/>
<exclude domain="file" path="cache/"/>
</cloud-backup>
<device-transfer>
<include domain="file" path="settings/"/>
<exclude domain="file" path="cache/"/>
</device-transfer>
</data-extraction-rules>
17 changes: 17 additions & 0 deletions qml/About.qml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,23 @@ Item {
sourceSize: 24
}

ListItem { // entitlement / Pro status
width: parent.width
visible: entitlement.stamped

text: entitlement.isPro ? qsTr("Theengs Pro — Active") : qsTr("Theengs Free")
source: "qrc:/IconLibrary/material-symbols/stars.svg"
sourceSize: 24

// HIL + a11y: the About screen is otherwise opaque Qt
// SurfaceView content; objectName + Accessible surface the
// entitlement state as a uiautomator-readable node
// (resource-id "hil-entitlement-status" + content-desc).
objectName: "hil-entitlement-status"
Accessible.role: Accessible.StaticText
Accessible.name: text
}

Item { height: 4; width: 4; } // spacer

////////
Expand Down
90 changes: 90 additions & 0 deletions src/Entitlement/Entitlement.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Theengs - Decode things and devices
Copyright: (c) Florian ROBERT

This file is part of Theengs.

Theengs is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Theengs is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include "Entitlement.h"
#include "EntitlementEras.h"
#include "ProvenanceStore.h"

#include <QCoreApplication>
#include <QSettings>
#include <QDateTime>
#include <QString>
#include <QDebug>

#if defined(Q_OS_ANDROID)
#include <QJniObject>
#include <QJniEnvironment>
#endif

namespace {

// Best-effort per-platform build integer. Used as a provenance breadcrumb
// only; not load-bearing for the current grant decision (the grant is
// unconditional — this binary is the paid build).
//
// Android: BuildConfig.VERSION_CODE — a primitive `int` static field
// generated by Gradle into the app's package namespace (com.theengs.app).
// The build.gradle in this repo sets versionCode = seconds-since-epoch at
// build time, so this integer is monotonically increasing per build and
// can later be compared against the boundaries in EntitlementEras.
//
// iOS/desktop: recorded as 0. On iOS, the Apple-held CFBundleVersion is
// available later via AppTransaction.originalAppVersion at decision time,
// so pre-recording it here would only be a breadcrumb.
int currentBuildNumber()
{
#if defined(Q_OS_ANDROID)
const jint code = QJniObject::getStaticField<jint>(
"com/theengs/app/BuildConfig", "VERSION_CODE");
QJniEnvironment env;
if (env.checkAndClearExceptions())
{
qWarning() << "Entitlement: failed to read BuildConfig.VERSION_CODE; recording 0";
return 0;
}
return static_cast<int>(code);
#else
return 0;
#endif
}

} // namespace

namespace Entitlement
{

void stampOnceAtStartup()
{
// Same QSettings keying as SettingsManager so the file is shared (and
// therefore in the same Android Auto-Backup set).
QSettings settings(QCoreApplication::organizationName(),
QCoreApplication::applicationName());

const auto &era = EntitlementEras::currentEraForStampRelease();
const qint64 nowMs = QDateTime::currentMSecsSinceEpoch();
const int build = currentBuildNumber();

ProvenanceStore::stampIfFirstLaunch(
settings,
nowMs,
build,
QString::fromLatin1(era.id),
QStringLiteral("GF_ANDROID_STAMP"));
}

} // namespace Entitlement
36 changes: 36 additions & 0 deletions src/Entitlement/Entitlement.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Theengs - Decode things and devices
Copyright: (c) Florian ROBERT

This file is part of Theengs.

Theengs is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Theengs is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef ENTITLEMENT_H
#define ENTITLEMENT_H
/* ************************************************************************** */

namespace Entitlement
{
//! Called once during app startup, immediately after SettingsManager is
//! initialized. Idempotent: writes the provenance stamp exactly once,
//! never overwrites an existing one. No-op on subsequent launches.
//!
//! Invariant: this binary is the PAID build. Every install executing
//! this code is, by definition, a legitimate paid_era buyer, so the
//! stamp grants Pro with source=GF_ANDROID_STAMP unconditionally.
void stampOnceAtStartup();
}

/* ************************************************************************** */
#endif // ENTITLEMENT_H
51 changes: 51 additions & 0 deletions src/Entitlement/EntitlementEras.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
Theengs - Decode things and devices
Copyright: (c) Florian ROBERT

This file is part of Theengs.

Theengs is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Theengs is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include "EntitlementEras.h"

namespace EntitlementEras
{

const Era kEras[] = {
// paid_era: every install up to and including the last paid build /
// pre-boundary.
{ "paid_era",
/*iosBuildMax=*/ kIosLastPaidBuild,
/*iosBuildMin=*/ 0,
/*androidBeforeEpochMs=*/ kAndroidFreeFlipEpochMs,
/*androidAfterEpochMs=*/ 0 },

// next_era: placeholder for the next cohort so the ledger ships with the
// forward boundary already shaped. Entitlements for this row are decided
// by the release that activates it, not by the current stamp release.
{ "next_era",
/*iosBuildMax=*/ 0,
/*iosBuildMin=*/ kIosFirstFreeBuild,
/*androidBeforeEpochMs=*/ 0,
/*androidAfterEpochMs=*/ kAndroidFreeFlipEpochMs },
};

const int kErasCount = static_cast<int>(sizeof(kEras) / sizeof(kEras[0]));

const Era &currentEraForStampRelease()
{
// kEras[0] is paid_era and is asserted by the unit test.
return kEras[0];
}

} // namespace EntitlementEras
70 changes: 70 additions & 0 deletions src/Entitlement/EntitlementEras.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
Theengs - Decode things and devices
Copyright: (c) Florian ROBERT

This file is part of Theengs.

Theengs is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Theengs is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef ENTITLEMENT_ERAS_H
#define ENTITLEMENT_ERAS_H
/* ************************************************************************** */

#include <QtGlobal>

// Boundary constants ledger. Append-only — never edit past rows; only append
// a new row at each future cohort boundary, freezing that boundary's build
// number / epoch in place. This file is the single source of truth that any
// future cohort decision reads.
//
// TODO(florian): Fill the four constants below from the store consoles
// before release. They are intentional placeholders — the engineer who
// authored this code cannot read the store consoles. The PR description
// must list these constants explicitly.
namespace EntitlementEras
{

// iOS: last paid CFBundleVersion. Read from App Store Connect.
constexpr int kIosLastPaidBuild = 0; // TODO(florian): fill from store console

// iOS: first CFBundleVersion of the next cohort. = kIosLastPaidBuild + 1.
constexpr int kIosFirstFreeBuild = 0; // TODO(florian): fill from store console

// Android: UTC epoch (ms) at the relevant cohort boundary. Set when the
// boundary becomes effective, not before.
constexpr qint64 kAndroidFreeFlipEpochMs = 0; // TODO(florian): fill at boundary

// Android: last paid Play versionCode. Informational; not used by the stamp
// logic, recorded here so future cohort math has it on hand.
constexpr int kAndroidLastPaidVersionCode = 0; // TODO(florian): fill from store console

struct Era
{
const char *id; //!< canonical era slug, written into provenance
int iosBuildMax; //!< <= boundary, 0 = N/A
int iosBuildMin; //!< >= boundary, 0 = N/A
qint64 androidBeforeEpochMs; //!< install < boundary, 0 = N/A
qint64 androidAfterEpochMs; //!< install >= boundary, 0 = N/A
};

extern const Era kEras[];
extern const int kErasCount;

//! Returns the era that the current stamp release must assign. Every install
//! running this binary is, by definition, in paid_era.
const Era &currentEraForStampRelease();

} // namespace EntitlementEras

/* ************************************************************************** */
#endif // ENTITLEMENT_ERAS_H
Loading
Loading