implement crypto

Signed-off-by: zyphlar <zyphlar@gmail.com>
This commit is contained in:
zyphlar
2025-10-20 17:25:51 -07:00
parent d430a2202e
commit 6c3710859b
11 changed files with 272 additions and 18 deletions

View File

@@ -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;
}
}
}

View File

@@ -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();
}
/**

View File

@@ -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");

View File

@@ -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)
{

View File

@@ -50,4 +50,21 @@
<item>8</item>
<item>10</item>
</string-array>
<!-- Location Sharing Update Intervals -->
<string-array name="location_sharing_intervals">
<item>5 seconds</item>
<item>10 seconds</item>
<item>20 seconds</item>
<item>30 seconds</item>
<item>60 seconds</item>
</string-array>
<string-array name="location_sharing_interval_values">
<item>5</item>
<item>10</item>
<item>20</item>
<item>30</item>
<item>60</item>
</string-array>
</resources>

View File

@@ -159,6 +159,27 @@
</intent>
</PreferenceScreen>
</androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory
android:key="@string/pref_location_sharing_category"
android:title="@string/location_sharing_title"
android:order="5">
<EditTextPreference
android:key="@string/pref_location_sharing_server_url"
android:title="@string/pref_location_sharing_server_url"
app:singleLineTitle="false"
android:summary="@string/pref_location_sharing_server_url_summary"
android:defaultValue="https://live.organicmaps.app"
android:order="1"/>
<ListPreference
android:key="@string/pref_location_sharing_update_interval"
android:title="@string/pref_location_sharing_update_interval"
app:singleLineTitle="false"
android:summary="@string/pref_location_sharing_update_interval_summary"
android:entries="@array/location_sharing_intervals"
android:entryValues="@array/location_sharing_interval_values"
android:defaultValue="20"
android:order="2"/>
</androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory
android:key="@string/pref_privacy"
android:title="@string/privacy"

View File

@@ -515,6 +515,46 @@ public final class Config
}
}
public static class LocationSharing
{
interface Keys
{
String SERVER_URL = "LocationSharingServerUrl";
String UPDATE_INTERVAL = "LocationSharingUpdateInterval";
}
public interface Defaults
{
String SERVER_URL = "https://live.organicmaps.app";
int UPDATE_INTERVAL = 20; // seconds
int UPDATE_INTERVAL_MIN = 5;
int UPDATE_INTERVAL_MAX = 60;
}
@NonNull
public static String getServerUrl()
{
return getString(Keys.SERVER_URL, Defaults.SERVER_URL);
}
public static void setServerUrl(@NonNull String url)
{
setString(Keys.SERVER_URL, url);
}
public static int getUpdateInterval()
{
return getInt(Keys.UPDATE_INTERVAL, Defaults.UPDATE_INTERVAL);
}
public static void setUpdateInterval(int seconds)
{
if (seconds < Defaults.UPDATE_INTERVAL_MIN || seconds > 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);

View File

@@ -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"
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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"