From 6c3710859be8c9856e55904cadbfafe032f158dc Mon Sep 17 00:00:00 2001 From: zyphlar Date: Mon, 20 Oct 2025 17:25:51 -0700 Subject: [PATCH] implement crypto Signed-off-by: zyphlar --- .../organicmaps/location/LocationCrypto.java | 131 ++++++++++++++++++ .../location/LocationSharingManager.java | 27 ++-- .../location/LocationSharingService.java | 18 ++- .../settings/SettingsPrefsFragment.java | 25 ++++ android/app/src/main/res/values/arrays.xml | 17 +++ android/app/src/main/res/xml/prefs_main.xml | 21 +++ .../java/app/organicmaps/sdk/util/Config.java | 40 ++++++ .../LocationSharingSession.swift | 2 + libs/platform/platform.cpp | 5 + libs/platform/platform.hpp | 3 + private.h | 1 + 11 files changed, 272 insertions(+), 18 deletions(-) create mode 100644 android/app/src/main/java/app/organicmaps/location/LocationCrypto.java diff --git a/android/app/src/main/java/app/organicmaps/location/LocationCrypto.java b/android/app/src/main/java/app/organicmaps/location/LocationCrypto.java new file mode 100644 index 000000000..f129d5512 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/location/LocationCrypto.java @@ -0,0 +1,131 @@ +package app.organicmaps.location; + +import android.util.Base64; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * AES-256-GCM encryption/decryption for location data. + */ +public class LocationCrypto +{ + private static final String ALGORITHM = "AES/GCM/NoPadding"; + private static final int GCM_IV_LENGTH = 12; // 96 bits + private static final int GCM_TAG_LENGTH = 128; // 128 bits + + /** + * Encrypt plaintext JSON using AES-256-GCM. + * @param base64Key Base64-encoded 256-bit key + * @param plaintextJson JSON string to encrypt + * @return JSON string with encrypted payload: {"iv":"...","ciphertext":"...","authTag":"..."} + */ + @Nullable + public static String encrypt(@NonNull String base64Key, @NonNull String plaintextJson) + { + try + { + // Decode the base64 key + byte[] key = Base64.decode(base64Key, Base64.NO_WRAP); + if (key.length != 32) // 256 bits + { + android.util.Log.e("LocationCrypto", "Invalid key size: " + key.length); + return null; + } + + // Generate random IV + byte[] iv = new byte[GCM_IV_LENGTH]; + new SecureRandom().nextBytes(iv); + + // Create cipher + Cipher cipher = Cipher.getInstance(ALGORITHM); + SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); + GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); + + // Encrypt + byte[] plaintext = plaintextJson.getBytes(StandardCharsets.UTF_8); + byte[] ciphertextWithTag = cipher.doFinal(plaintext); + + // Split ciphertext and auth tag + // In GCM mode, doFinal() returns ciphertext + tag + int ciphertextLength = ciphertextWithTag.length - (GCM_TAG_LENGTH / 8); + byte[] ciphertext = new byte[ciphertextLength]; + byte[] authTag = new byte[GCM_TAG_LENGTH / 8]; + + System.arraycopy(ciphertextWithTag, 0, ciphertext, 0, ciphertextLength); + System.arraycopy(ciphertextWithTag, ciphertextLength, authTag, 0, authTag.length); + + // Build JSON response + JSONObject result = new JSONObject(); + result.put("iv", Base64.encodeToString(iv, Base64.NO_WRAP)); + result.put("ciphertext", Base64.encodeToString(ciphertext, Base64.NO_WRAP)); + result.put("authTag", Base64.encodeToString(authTag, Base64.NO_WRAP)); + + return result.toString(); + } + catch (Exception e) + { + android.util.Log.e("LocationCrypto", "Encryption failed", e); + return null; + } + } + + /** + * Decrypt encrypted payload using AES-256-GCM. + * @param base64Key Base64-encoded 256-bit key + * @param encryptedPayloadJson JSON string with format: {"iv":"...","ciphertext":"...","authTag":"..."} + * @return Decrypted plaintext JSON string + */ + @Nullable + public static String decrypt(@NonNull String base64Key, @NonNull String encryptedPayloadJson) + { + try + { + // Parse encrypted payload + JSONObject payload = new JSONObject(encryptedPayloadJson); + byte[] iv = Base64.decode(payload.getString("iv"), Base64.NO_WRAP); + byte[] ciphertext = Base64.decode(payload.getString("ciphertext"), Base64.NO_WRAP); + byte[] authTag = Base64.decode(payload.getString("authTag"), Base64.NO_WRAP); + + // Decode the base64 key + byte[] key = Base64.decode(base64Key, Base64.NO_WRAP); + if (key.length != 32) // 256 bits + { + android.util.Log.e("LocationCrypto", "Invalid key size: " + key.length); + return null; + } + + // Combine ciphertext and auth tag for GCM decryption + byte[] ciphertextWithTag = new byte[ciphertext.length + authTag.length]; + System.arraycopy(ciphertext, 0, ciphertextWithTag, 0, ciphertext.length); + System.arraycopy(authTag, 0, ciphertextWithTag, ciphertext.length, authTag.length); + + // Create cipher + Cipher cipher = Cipher.getInstance(ALGORITHM); + SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); + GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec); + + // Decrypt + byte[] plaintext = cipher.doFinal(ciphertextWithTag); + + return new String(plaintext, StandardCharsets.UTF_8); + } + catch (Exception e) + { + android.util.Log.e("LocationCrypto", "Decryption failed", e); + return null; + } + } +} diff --git a/android/app/src/main/java/app/organicmaps/location/LocationSharingManager.java b/android/app/src/main/java/app/organicmaps/location/LocationSharingManager.java index 978543cbc..f4c3a0718 100644 --- a/android/app/src/main/java/app/organicmaps/location/LocationSharingManager.java +++ b/android/app/src/main/java/app/organicmaps/location/LocationSharingManager.java @@ -9,6 +9,7 @@ import androidx.annotation.Nullable; import app.organicmaps.MwmApplication; import app.organicmaps.sdk.routing.RoutingController; +import app.organicmaps.sdk.util.Config; import app.organicmaps.sdk.util.log.Logger; /** @@ -31,10 +32,6 @@ public class LocationSharingManager private final Context mContext; - // Configuration - private int mUpdateIntervalSeconds = 20; - private String mServerBaseUrl = "https://live.organicmaps.app"; // TODO: Configure - private LocationSharingManager() { mContext = MwmApplication.sInstance; @@ -72,8 +69,9 @@ public class LocationSharingManager mSessionId = credentials[0]; mEncryptionKey = credentials[1]; - // Generate share URL - mShareUrl = nativeGenerateShareUrl(mSessionId, mEncryptionKey, mServerBaseUrl); + // Generate share URL using configured server + String serverUrl = Config.LocationSharing.getServerUrl(); + mShareUrl = nativeGenerateShareUrl(mSessionId, mEncryptionKey, serverUrl); if (mShareUrl == null) { Logger.e(TAG, "Failed to generate share URL"); @@ -86,8 +84,8 @@ public class LocationSharingManager Intent intent = new Intent(mContext, LocationSharingService.class); intent.putExtra(LocationSharingService.EXTRA_SESSION_ID, mSessionId); intent.putExtra(LocationSharingService.EXTRA_ENCRYPTION_KEY, mEncryptionKey); - intent.putExtra(LocationSharingService.EXTRA_SERVER_URL, mServerBaseUrl); - intent.putExtra(LocationSharingService.EXTRA_UPDATE_INTERVAL, mUpdateIntervalSeconds); + intent.putExtra(LocationSharingService.EXTRA_SERVER_URL, serverUrl); + intent.putExtra(LocationSharingService.EXTRA_UPDATE_INTERVAL, Config.LocationSharing.getUpdateInterval()); mContext.startForegroundService(intent); @@ -138,28 +136,23 @@ public class LocationSharingManager public void setUpdateIntervalSeconds(int seconds) { - if (seconds < 5 || seconds > 60) - { - Logger.w(TAG, "Invalid update interval: " + seconds + ", using default"); - return; - } - mUpdateIntervalSeconds = seconds; + Config.LocationSharing.setUpdateInterval(seconds); } public int getUpdateIntervalSeconds() { - return mUpdateIntervalSeconds; + return Config.LocationSharing.getUpdateInterval(); } public void setServerBaseUrl(@NonNull String url) { - mServerBaseUrl = url; + Config.LocationSharing.setServerUrl(url); } @NonNull public String getServerBaseUrl() { - return mServerBaseUrl; + return Config.LocationSharing.getServerUrl(); } /** diff --git a/android/app/src/main/java/app/organicmaps/location/LocationSharingService.java b/android/app/src/main/java/app/organicmaps/location/LocationSharingService.java index 6757724b9..b1ba73e29 100644 --- a/android/app/src/main/java/app/organicmaps/location/LocationSharingService.java +++ b/android/app/src/main/java/app/organicmaps/location/LocationSharingService.java @@ -111,6 +111,22 @@ public class LocationSharingService extends Service implements LocationListener // Initialize API client mApiClient = new LocationSharingApiClient(mServerUrl, mSessionId); + // Create session on server + mApiClient.createSession(new LocationSharingApiClient.Callback() + { + @Override + public void onSuccess() + { + Logger.i(TAG, "Session created on server"); + } + + @Override + public void onFailure(@NonNull String error) + { + Logger.w(TAG, "Failed to create session on server: " + error); + } + }); + // Start foreground with notification Notification notification = mNotificationHelper != null ? mNotificationHelper.buildNotification(getStopIntent()) @@ -212,7 +228,7 @@ public class LocationSharingService extends Service implements LocationListener return; // Encrypt payload - String encryptedJson = LocationSharingManager.nativeEncryptPayload(mEncryptionKey, payload.toString()); + String encryptedJson = LocationCrypto.encrypt(mEncryptionKey, payload.toString()); if (encryptedJson == null) { Logger.e(TAG, "Failed to encrypt payload"); diff --git a/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java b/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java index 26eade991..b672d7e67 100644 --- a/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java +++ b/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java @@ -8,6 +8,7 @@ import android.os.Bundle; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.preference.EditTextPreference; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; @@ -73,6 +74,7 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La initScreenSleepEnabledPrefsCallbacks(); initShowOnLockScreenPrefsCallbacks(); initLeftButtonPrefs(); + initLocationSharingPrefsCallbacks(); } private void initLeftButtonPrefs() @@ -542,6 +544,29 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La category.removePreference(preference); } + private void initLocationSharingPrefsCallbacks() + { + // Server URL preference + final EditTextPreference serverUrlPref = getPreference(getString(R.string.pref_location_sharing_server_url)); + serverUrlPref.setText(Config.LocationSharing.getServerUrl()); + serverUrlPref.setSummary(Config.LocationSharing.getServerUrl()); + serverUrlPref.setOnPreferenceChangeListener((preference, newValue) -> { + String url = (String) newValue; + Config.LocationSharing.setServerUrl(url); + serverUrlPref.setSummary(url); + return true; + }); + + // Update interval preference + final ListPreference intervalPref = getPreference(getString(R.string.pref_location_sharing_update_interval)); + intervalPref.setValue(String.valueOf(Config.LocationSharing.getUpdateInterval())); + intervalPref.setOnPreferenceChangeListener((preference, newValue) -> { + int seconds = Integer.parseInt((String) newValue); + Config.LocationSharing.setUpdateInterval(seconds); + return true; + }); + } + @Override public void onLanguageSelected(Language language) { diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml index de2c651d7..1df52df4a 100644 --- a/android/app/src/main/res/values/arrays.xml +++ b/android/app/src/main/res/values/arrays.xml @@ -50,4 +50,21 @@ 8 10 + + + + 5 seconds + 10 seconds + 20 seconds + 30 seconds + 60 seconds + + + + 5 + 10 + 20 + 30 + 60 + diff --git a/android/app/src/main/res/xml/prefs_main.xml b/android/app/src/main/res/xml/prefs_main.xml index 32964f38b..9c5f66492 100644 --- a/android/app/src/main/res/xml/prefs_main.xml +++ b/android/app/src/main/res/xml/prefs_main.xml @@ -159,6 +159,27 @@ + + + + Defaults.UPDATE_INTERVAL_MAX) + seconds = Defaults.UPDATE_INTERVAL; + setInt(Keys.UPDATE_INTERVAL, seconds); + } + } + private static native boolean nativeHasConfigValue(String name); private static native boolean nativeDeleteConfigValue(String name); private static native boolean nativeGetBoolean(String name, boolean defaultValue); diff --git a/iphone/Maps/Core/LocationSharing/LocationSharingSession.swift b/iphone/Maps/Core/LocationSharing/LocationSharingSession.swift index ab03a3f98..18b2b1e55 100644 --- a/iphone/Maps/Core/LocationSharing/LocationSharingSession.swift +++ b/iphone/Maps/Core/LocationSharing/LocationSharingSession.swift @@ -23,6 +23,8 @@ struct LocationSharingConfig { var includeDestinationName: Bool = true var includeBatteryLevel: Bool = true var lowBatteryThreshold: Int = 10 + // Default from LOCATION_SHARING_SERVER_URL in private.h + // Can be overridden by user settings var serverBaseUrl: String = "https://live.organicmaps.app" } diff --git a/libs/platform/platform.cpp b/libs/platform/platform.cpp index 906758520..1de5d9946 100644 --- a/libs/platform/platform.cpp +++ b/libs/platform/platform.cpp @@ -158,6 +158,11 @@ std::string Platform::DefaultUrlsJSON() const return DEFAULT_URLS_JSON; } +std::string Platform::LocationSharingServerUrl() const +{ + return LOCATION_SHARING_SERVER_URL; +} + bool Platform::RemoveFileIfExists(std::string const & filePath) { return IsFileExistsByFullPath(filePath) ? base::DeleteFileX(filePath) : true; diff --git a/libs/platform/platform.hpp b/libs/platform/platform.hpp index 10bc76403..65aa5217c 100644 --- a/libs/platform/platform.hpp +++ b/libs/platform/platform.hpp @@ -271,6 +271,9 @@ public: /// @return JSON-encoded list of urls if metaserver is unreachable std::string DefaultUrlsJSON() const; + /// @return default location sharing server URL + std::string LocationSharingServerUrl() const; + bool IsTablet() const { return m_isTablet; } /// @return information about kinds of memory which are relevant for a platform. diff --git a/private.h b/private.h index 870f46178..3bfb82735 100644 --- a/private.h +++ b/private.h @@ -11,3 +11,4 @@ #define TRAFFIC_DATA_BASE_URL "" #define USER_BINDING_PKCS12 "" #define USER_BINDING_PKCS12_PASSWORD "" +#define LOCATION_SHARING_SERVER_URL "https://ec1e1096-e991-4cb1-ac21-30fbad2bd406.mock.pstmn.io"