Panagiotis Triantafyllou

migrate to maven central, db fixes for android 15

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
<bytecodeTargetLevel target="21" />
</component>
</project>
\ No newline at end of file
......
......@@ -3,6 +3,7 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
......
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
</project>
\ No newline at end of file
......
......@@ -2,13 +2,14 @@ apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
android {
compileSdkVersion 33
buildToolsVersion "33.0.2"
compileSdkVersion 35
buildToolsVersion "35.0.0"
namespace "warp.ly.android_sdk"
defaultConfig {
applicationId "warp.ly.android_sdk"
minSdkVersion 23
targetSdkVersion 33
targetSdkVersion 35
versionCode 100
versionName "1.0.0"
}
......
......@@ -4,7 +4,7 @@
Uuid=b13ade8ef743468b89a7aaa8efbfc468
# If we need to see logs in Logcat
Debug=false
Debug=true
# Production or Development environment of the engage server
# Production: https://engage.warp.ly
......
......@@ -8,16 +8,19 @@ buildscript {
maven { url 'https://plugins.gradle.org/m2/' }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.4'
classpath 'com.google.gms:google-services:4.3.15'
classpath 'com.huawei.agconnect:agcp:1.7.2.300'
classpath 'io.github.gradle-nexus:publish-plugin:1.1.0'
classpath 'com.android.tools.build:gradle:8.7.3'
classpath 'com.google.gms:google-services:4.4.3'
classpath 'com.huawei.agconnect:agcp:1.9.3.300'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
plugins {
id 'maven-publish'
}
allprojects {
repositories {
mavenCentral()
......@@ -27,5 +30,4 @@ allprojects {
}
}
apply plugin: 'io.github.gradle-nexus.publish-plugin'
apply from: "${rootDir}/scripts/publish-root.gradle"
\ No newline at end of file
......
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
......
......@@ -32,15 +32,15 @@ afterEvaluate {
// Two artifacts, the `aar` (or `jar`) and the sources
if (project.plugins.findPlugin("com.android.library")) {
from components.release
from(project.components.findByName("release"))
} else {
from components.java
}
artifact androidSourcesJar
// Sources are now handled by the android block's singleVariant
// artifact javadocJar
// Mostly self-explanatory metadata
// POM metadata for Maven Central
pom {
name = PUBLISH_ARTIFACT_ID
description = 'Warply Android SDK Maven Plugin'
......@@ -60,8 +60,7 @@ afterEvaluate {
// Add all other devs here...
}
// Version control info - if you're using GitHub, follow the
// format as seen here
// Version control info
scm {
connection = 'scm:git:git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
developerConnection = 'scm:git:ssh://git.warp.ly/open-source/warply_android_sdk_maven_plugin.git'
......@@ -80,4 +79,366 @@ signing {
rootProject.ext["signing.password"],
)
sign publishing.publications
}
\ No newline at end of file
}
// Configuration for Central Publishing (similar to Maven plugin configuration)
ext.centralPublishing = [
autoPublish : false, // Manual publishing for safety
// waitUntil: "published", // Commented out - don't wait for publishing
deploymentName: "Warply Android SDK ${PUBLISH_VERSION}",
centralBaseUrl: "https://central.sonatype.com"
]
// Custom task that implements the same functionality as org.sonatype.central:central-publishing-maven-plugin:0.8.0
// This uses the Central Portal API directly to achieve the same result
task publishToCentralPortal {
dependsOn 'publishReleasePublicationToMavenLocal'
description = 'Publishes to Maven Central Portal using the same API as central-publishing-maven-plugin:0.8.0'
group = 'publishing'
doLast {
def username = rootProject.ext["centralPortalUsername"]
def password = rootProject.ext["centralPortalPassword"]
def config = project.ext.centralPublishing
if (!username || !password) {
throw new GradleException("Central Portal credentials not configured. Please set centralPortalUsername and centralPortalPassword in local.properties or environment variables.")
}
println "=== Central Portal Publishing ==="
println "Deployment: ${config.deploymentName}"
println "Auto-publish: ${config.autoPublish}"
println "Portal URL: ${config.centralBaseUrl}"
println ""
// Step 1: Create deployment bundle
println "Step 1: Creating deployment bundle..."
def bundleFile = createDeploymentBundle()
println "✓ Bundle created: ${bundleFile.name} (${bundleFile.length()} bytes)"
// Step 2: Upload bundle to Central Portal
println "\nStep 2: Uploading to Central Portal..."
def deploymentId = uploadBundle(bundleFile, username, password, config)
println "✓ Upload successful. Deployment ID: ${deploymentId}"
// Step 3: Wait for validation
println "\nStep 3: Waiting for validation..."
def validationResult = waitForValidation(deploymentId, username, password, config)
if (validationResult.success) {
def state = validationResult.state
println "✓ Validation successful! State: ${state}"
if (config.autoPublish && state == "VALIDATED") {
println "\nStep 4: Auto-publishing..."
def publishResult = publishDeployment(deploymentId, username, password, config)
if (publishResult.success) {
println "✓ Published successfully to Maven Central!"
} else {
throw new GradleException("Auto-publishing failed: ${publishResult.error}")
}
} else if (state == "PUBLISHED") {
println "✓ Already published to Maven Central!"
def response = validationResult.response
def purls = response.purls ?: []
if (purls) {
println " Published artifacts:"
purls.each { purl -> println " - ${purl}" }
}
} else {
println "\n✓ Deployment uploaded and validated successfully!"
println "📋 Manual action required:"
println " Visit: ${config.centralBaseUrl}/publishing/deployments"
println " Find deployment: ${config.deploymentName}"
println " Click 'Publish' to complete the process"
}
} else {
throw new GradleException("Validation failed: ${validationResult.error}")
}
println "\n=== Publishing Complete ==="
}
}
def createDeploymentBundle() {
def bundleDir = file("${buildDir}/central-publishing")
def stagingDir = file("${bundleDir}/staging")
// Clean and create directories
bundleDir.deleteDir()
stagingDir.mkdirs()
// Create Maven repository structure
def groupPath = PUBLISH_GROUP_ID.replace('.', '/')
def artifactDir = file("${stagingDir}/${groupPath}/${PUBLISH_ARTIFACT_ID}/${PUBLISH_VERSION}")
artifactDir.mkdirs()
// Copy artifacts to staging area
def artifacts = [:]
// AAR file
def aarFile = file("${buildDir}/outputs/aar/warply_android_sdk-release.aar")
if (aarFile.exists()) {
def targetAar = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.aar")
copy {
from aarFile
into artifactDir
rename { targetAar.name }
}
artifacts['aar'] = targetAar
// Copy AAR signature if exists
def aarSigFile = file("${aarFile.path}.asc")
if (aarSigFile.exists()) {
def targetAarSig = file("${targetAar.path}.asc")
copy {
from aarSigFile
into artifactDir
rename { targetAarSig.name }
}
artifacts['aar-sig'] = targetAarSig
}
}
// Sources JAR
def sourcesFile = file("${buildDir}/libs/warply_android_sdk-${PUBLISH_VERSION}-sources.jar")
if (sourcesFile.exists()) {
def targetSources = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}-sources.jar")
copy {
from sourcesFile
into artifactDir
rename { targetSources.name }
}
artifacts['sources'] = targetSources
// Copy sources signature if exists
def sourcesSigFile = file("${sourcesFile.path}.asc")
if (sourcesSigFile.exists()) {
def targetSourcesSig = file("${targetSources.path}.asc")
copy {
from sourcesSigFile
into artifactDir
rename { targetSourcesSig.name }
}
artifacts['sources-sig'] = targetSourcesSig
}
}
// POM file
def pomFile = file("${buildDir}/publications/release/pom-default.xml")
if (pomFile.exists()) {
def targetPom = file("${artifactDir}/${PUBLISH_ARTIFACT_ID}-${PUBLISH_VERSION}.pom")
copy {
from pomFile
into artifactDir
rename { targetPom.name }
}
artifacts['pom'] = targetPom
// Copy POM signature if exists
def pomSigFile = file("${pomFile.path}.asc")
if (pomSigFile.exists()) {
def targetPomSig = file("${targetPom.path}.asc")
copy {
from pomSigFile
into artifactDir
rename { targetPomSig.name }
}
artifacts['pom-sig'] = targetPomSig
}
}
// Generate checksums for all files
artifacts.each { type, artifactFile ->
if (artifactFile.exists()) {
generateChecksums(artifactFile)
}
}
// Create bundle ZIP
def bundleFile = file("${bundleDir}/central-bundle.zip")
ant.zip(destfile: bundleFile) {
fileset(dir: stagingDir)
}
return bundleFile
}
def generateChecksums(File file) {
['md5', 'sha1', 'sha256', 'sha512'].each { algorithm ->
def checksum = file.withInputStream { stream ->
java.security.MessageDigest.getInstance(algorithm.toUpperCase()).digest(stream.bytes).encodeHex().toString()
}
new File("${file.path}.${algorithm}").text = checksum
}
}
def uploadBundle(File bundleFile, String username, String password, Map config) {
def url = "${config.centralBaseUrl}/api/v1/publisher/upload"
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
// Add query parameters
def publishingType = config.autoPublish ? "AUTOMATIC" : "USER_MANAGED"
def urlWithParams = "${url}?publishingType=${publishingType}&name=${URLEncoder.encode(config.deploymentName, 'UTF-8')}"
println " Uploading to: ${urlWithParams}"
println " Publishing type: ${publishingType}"
def connection = new URL(urlWithParams).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
connection.setDoOutput(true)
connection.setDoInput(true)
// Create multipart/form-data boundary
def boundary = "----WebKitFormBoundary" + System.currentTimeMillis()
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=${boundary}")
// Write multipart data
connection.outputStream.withWriter("UTF-8") { writer ->
writer.write("--${boundary}\r\n")
writer.write("Content-Disposition: form-data; name=\"bundle\"; filename=\"${bundleFile.name}\"\r\n")
writer.write("Content-Type: application/octet-stream\r\n")
writer.write("\r\n")
writer.flush()
// Write file content
bundleFile.withInputStream { input ->
connection.outputStream << input
}
writer.write("\r\n--${boundary}--\r\n")
writer.flush()
}
// Get response
def responseCode = connection.responseCode
if (responseCode == 201) {
def deploymentId = connection.inputStream.text.trim()
println " ✓ Upload successful (HTTP ${responseCode})"
return deploymentId
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Upload failed (HTTP ${responseCode}): ${errorMessage}")
}
}
def waitForValidation(String deploymentId, String username, String password, Map config) {
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
def maxAttempts = 60 // 5 minutes with 5-second intervals
def attempt = 0
while (attempt < maxAttempts) {
attempt++
def url = "${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
def connection = new URL(url).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
connection.setRequestProperty("Content-Type", "application/json")
def responseCode = connection.responseCode
if (responseCode == 200) {
def response = new groovy.json.JsonSlurper().parseText(connection.inputStream.text)
def state = response.deploymentState
println " Status check ${attempt}: ${state}"
switch (state) {
case "PENDING":
case "VALIDATING":
// Continue waiting
Thread.sleep(5000)
break
case "VALIDATED":
return [success: true, state: state, response: response]
case "PUBLISHED":
return [success: true, state: state, response: response]
case "FAILED":
def errors = response.errors ?: ["Unknown validation error"]
return [success: false, error: "Validation failed: ${errors.join(', ')}", response: response]
default:
return [success: false, error: "Unknown deployment state: ${state}", response: response]
}
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Status check failed (HTTP ${responseCode}): ${errorMessage}")
}
}
return [success: false, error: "Timeout waiting for validation (${maxAttempts * 5} seconds)"]
}
def publishDeployment(String deploymentId, String username, String password, Map config) {
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
def url = "${config.centralBaseUrl}/api/v1/publisher/deployment/${deploymentId}"
println " Calling publish API..."
def connection = new URL(url).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
def responseCode = connection.responseCode
if (responseCode == 204) {
println " ✓ Publish request successful (HTTP ${responseCode})"
// Wait for publishing to complete
println " Waiting for publishing to complete..."
def result = waitForPublishing(deploymentId, username, password, config)
return result
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Publish failed (HTTP ${responseCode}): ${errorMessage}")
}
}
def waitForPublishing(String deploymentId, String username, String password, Map config) {
def credentials = "${username}:${password}".bytes.encodeBase64().toString()
def maxAttempts = 120 // 10 minutes with 5-second intervals
def attempt = 0
while (attempt < maxAttempts) {
attempt++
def url = "${config.centralBaseUrl}/api/v1/publisher/status?id=${deploymentId}"
def connection = new URL(url).openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setRequestProperty("Authorization", "Bearer ${credentials}")
connection.setRequestProperty("Content-Type", "application/json")
def responseCode = connection.responseCode
if (responseCode == 200) {
def response = new groovy.json.JsonSlurper().parseText(connection.inputStream.text)
def state = response.deploymentState
println " Publishing status ${attempt}: ${state}"
switch (state) {
case "PUBLISHING":
// Continue waiting
Thread.sleep(5000)
break
case "PUBLISHED":
def purls = response.purls ?: []
println " ✓ Successfully published to Maven Central!"
if (purls) {
println " Published artifacts:"
purls.each { purl -> println " - ${purl}" }
}
return [success: true, state: state, response: response]
case "FAILED":
def errors = response.errors ?: ["Unknown publishing error"]
return [success: false, error: "Publishing failed: ${errors.join(', ')}", response: response]
default:
return [success: false, error: "Unexpected state during publishing: ${state}", response: response]
}
} else {
def errorMessage = connection.errorStream?.text ?: "Unknown error"
throw new GradleException("Publishing status check failed (HTTP ${responseCode}): ${errorMessage}")
}
}
return [success: false, error: "Timeout waiting for publishing (${maxAttempts * 5} seconds)"]
}
......
// Create variables with empty default values
// Central Portal credentials
ext["centralPortalUsername"] = ''
ext["centralPortalPassword"] = ''
// keyId is the last 8 characters of the GPG key
ext["signing.keyId"] = ''
// password is the passphrase of the GPG key
ext["signing.password"] = ''
// key is the base64 private GPG key
ext["signing.key"] = ''
// osshrUsername and ossrhPassword are the account details for MavenCentral
// which we’ve chosen at the Jira registration step (Sonatype site))
ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
......@@ -19,24 +18,11 @@ if (secretPropsFile.exists()) {
new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
p.each { name, value -> ext[name] = value }
} else {
// Use system environment variables
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
// Use system environment variables for signing
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.key"] = System.getenv('SIGNING_KEY')
// Central Portal credentials can also come from environment
ext["centralPortalUsername"] = System.getenv('CENTRAL_PORTAL_USERNAME')
ext["centralPortalPassword"] = System.getenv('CENTRAL_PORTAL_PASSWORD')
}
// Set up Sonatype repository
nexusPublishing {
repositories {
sonatype {
stagingProfileId = sonatypeStagingProfileId
username = ossrhUsername
password = ossrhPassword
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
}
}
}
\ No newline at end of file
......
......@@ -7,22 +7,18 @@ import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import net.sqlcipher.DatabaseUtils;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteOpenHelper;
import net.sqlcipher.database.SQLiteStatement;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import ly.warp.sdk.utils.constants.WarpConstants;
import ly.warp.sdk.utils.CryptoUtils;
public class WarplyDBHelper extends SQLiteOpenHelper {
......@@ -30,13 +26,8 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
// Constants
// ===========================================================
private enum State {
DOES_NOT_EXIST, UNENCRYPTED, ENCRYPTED
}
private static final String DB_NAME = "warply.db";
private static final int DB_VERSION = 5;
private static final String KEY_CIPHER = "tn#mpOl3v3Dy1pr@W";
private static final String DB_NAME = "warply_v2.db";
private static final int DB_VERSION = 1;
//------------------------------ Fields -----------------------------//
private static String TABLE_REQUESTS = "requests";
......@@ -110,7 +101,6 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
public static synchronized WarplyDBHelper getInstance(Context context) {
if (mDBHelperInstance == null) {
SQLiteDatabase.loadLibs(context);
mDBHelperInstance = new WarplyDBHelper(context);
}
return mDBHelperInstance;
......@@ -118,19 +108,15 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
private WarplyDBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
State tempDatabaseState = getDatabaseState(context, DB_NAME);
if (tempDatabaseState.equals(State.UNENCRYPTED)) {
encrypt(context, context.getDatabasePath(DB_NAME), KEY_CIPHER.getBytes());
}
}
/**
* If database connection already initialized, return the db. Else create a
* new one
* Get standard SQLite database connection
*/
private SQLiteDatabase getDb() {
if (mDb == null)
mDb = getWritableDatabase(KEY_CIPHER);
if (mDb == null || !mDb.isOpen()) {
mDb = getWritableDatabase();
}
return mDb;
}
......@@ -215,13 +201,13 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
return false;
}
//------------------------------ Auth -----------------------------//
//------------------------------ Auth (with field-level encryption) -----------------------------//
public synchronized void saveClientAccess(String clientId, String clientSecret) {
ContentValues values = new ContentValues();
if (!TextUtils.isEmpty(clientId))
values.put(KEY_CLIENT_ID, clientId);
values.put(KEY_CLIENT_ID, CryptoUtils.encrypt(clientId));
if (!TextUtils.isEmpty(clientSecret))
values.put(KEY_CLIENT_SECRET, clientSecret);
values.put(KEY_CLIENT_SECRET, CryptoUtils.encrypt(clientSecret));
if (isTableNotEmpty(TABLE_CLIENT))
update(TABLE_CLIENT, values);
else
......@@ -231,9 +217,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
public synchronized void saveAuthAccess(String accessToken, String refreshToken) {
ContentValues values = new ContentValues();
if (!TextUtils.isEmpty(accessToken))
values.put(KEY_ACCESS_TOKEN, accessToken);
values.put(KEY_ACCESS_TOKEN, CryptoUtils.encrypt(accessToken));
if (!TextUtils.isEmpty(refreshToken))
values.put(KEY_REFRESH_TOKEN, refreshToken);
values.put(KEY_REFRESH_TOKEN, CryptoUtils.encrypt(refreshToken));
if (isTableNotEmpty(TABLE_AUTH))
update(TABLE_AUTH, values);
else
......@@ -244,9 +230,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
public synchronized String getAuthValue(String columnName) {
String columnValue = "";
Cursor cursor = getDb().query(TABLE_AUTH, new String[]{columnName}, null, null, null, null, null);
if (cursor != null) {
cursor.moveToFirst();
columnValue = cursor.getString(cursor.getColumnIndex(columnName));
if (cursor != null && cursor.moveToFirst()) {
String encryptedValue = cursor.getString(cursor.getColumnIndex(columnName));
columnValue = CryptoUtils.decrypt(encryptedValue);
cursor.close();
}
return columnValue;
......@@ -256,9 +242,9 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
public synchronized String getClientValue(String columnName) {
String columnValue = "";
Cursor cursor = getDb().query(TABLE_CLIENT, new String[]{columnName}, null, null, null, null, null);
if (cursor != null) {
cursor.moveToFirst();
columnValue = cursor.getString(cursor.getColumnIndex(columnName));
if (cursor != null && cursor.moveToFirst()) {
String encryptedValue = cursor.getString(cursor.getColumnIndex(columnName));
columnValue = CryptoUtils.decrypt(encryptedValue);
cursor.close();
}
return columnValue;
......@@ -343,15 +329,15 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
}
public synchronized long getRequestsInQueueCount() {
return DatabaseUtils.queryNumEntries(getReadableDatabase(KEY_CIPHER), TABLE_REQUESTS);
return DatabaseUtils.queryNumEntries(getReadableDatabase(), TABLE_REQUESTS);
}
public synchronized long getPushRequestsInQueueCount() {
return DatabaseUtils.queryNumEntries(getReadableDatabase(KEY_CIPHER), TABLE_PUSH_REQUESTS);
return DatabaseUtils.queryNumEntries(getReadableDatabase(), TABLE_PUSH_REQUESTS);
}
public synchronized long getPushAckRequestsInQueueCount() {
return DatabaseUtils.queryNumEntries(getReadableDatabase(KEY_CIPHER), TABLE_PUSH_ACK_REQUESTS);
return DatabaseUtils.queryNumEntries(getReadableDatabase(), TABLE_PUSH_ACK_REQUESTS);
}
public synchronized void deleteRequests(Long... ids) {
......@@ -483,73 +469,11 @@ public class WarplyDBHelper extends SQLiteOpenHelper {
tags = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
tags.add(cursor.getString(cursor
.getColumnIndex(KEY_TAG)));
tags.add(cursor.getString(cursor.getColumnIndex(KEY_TAG)));
}
cursor.close();
}
return tags != null ? tags.toArray(new String[tags.size()]) : null;
}
private State getDatabaseState(Context context, String dbName) {
SQLiteDatabase.loadLibs(context);
return (getDatabaseState(context.getDatabasePath(dbName)));
}
private static State getDatabaseState(File dbPath) {
if (dbPath.exists()) {
SQLiteDatabase db = null;
try {
db = SQLiteDatabase.openDatabase(dbPath.getAbsolutePath(), "", null, SQLiteDatabase.OPEN_READONLY);
db.getVersion();
return (State.UNENCRYPTED);
} catch (Exception e) {
return (State.ENCRYPTED);
} finally {
if (db != null) {
db.close();
}
}
}
return (State.DOES_NOT_EXIST);
}
private void encrypt(Context context, File originalFile, byte[] passphrase) {
SQLiteDatabase.loadLibs(context);
try {
if (originalFile.exists()) {
File newFile = File.createTempFile("sqlcipherutils", "tmp", context.getCacheDir());
SQLiteDatabase db = SQLiteDatabase.openDatabase(originalFile.getAbsolutePath(),
"", null, SQLiteDatabase.OPEN_READWRITE);
int version = db.getVersion();
db.close();
db = SQLiteDatabase.openDatabase(newFile.getAbsolutePath(), passphrase,
null, SQLiteDatabase.OPEN_READWRITE, null, null);
final SQLiteStatement st = db.compileStatement("ATTACH DATABASE ? AS plaintext KEY ''");
st.bindString(1, originalFile.getAbsolutePath());
st.execute();
db.rawExecSQL("SELECT sqlcipher_export('main', 'plaintext')");
db.rawExecSQL("DETACH DATABASE plaintext");
db.setVersion(version);
st.close();
db.close();
originalFile.delete();
newFile.renameTo(originalFile);
} else {
throw new FileNotFoundException(originalFile.getAbsolutePath() + " not found");
}
} catch (IOException ex) {
Log.v("WarplyDB Exception: ", ex.getMessage());
}
}
}
......
package ly.warp.sdk.utils;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.util.Log;
import java.security.KeyStore;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
/**
* Utility class for encrypting and decrypting sensitive data using Android Keystore
*/
public class CryptoUtils {
private static final String TAG = "CryptoUtils";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
private static final String KEY_ALIAS = "tn#mpOl3v3Dy1pr@W";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
/**
* Encrypt a plain text string
*
* @param plainText The text to encrypt
* @return Base64 encoded encrypted string, or original text if encryption fails
*/
public static String encrypt(String plainText) {
if (plainText == null || plainText.isEmpty()) {
return plainText;
}
try {
SecretKey secretKey = getOrCreateSecretKey();
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] iv = cipher.getIV();
byte[] encryptedData = cipher.doFinal(plainText.getBytes("UTF-8"));
// Combine IV + encrypted data
byte[] combined = new byte[iv.length + encryptedData.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encryptedData, 0, combined, iv.length, encryptedData.length);
return Base64.encodeToString(combined, Base64.DEFAULT);
} catch (Exception e) {
Log.e(TAG, "Encryption failed, returning original text", e);
// Return original text if encryption fails - graceful degradation
return plainText;
}
}
/**
* Decrypt an encrypted string
*
* @param encryptedText The Base64 encoded encrypted text
* @return Decrypted plain text, or original text if decryption fails
*/
public static String decrypt(String encryptedText) {
if (encryptedText == null || encryptedText.isEmpty()) {
return encryptedText;
}
try {
SecretKey secretKey = getOrCreateSecretKey();
byte[] combined = Base64.decode(encryptedText, Base64.DEFAULT);
// Extract IV and encrypted data
byte[] iv = new byte[GCM_IV_LENGTH];
byte[] encryptedData = new byte[combined.length - GCM_IV_LENGTH];
System.arraycopy(combined, 0, iv, 0, GCM_IV_LENGTH);
System.arraycopy(combined, GCM_IV_LENGTH, encryptedData, 0, encryptedData.length);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
byte[] decryptedData = cipher.doFinal(encryptedData);
return new String(decryptedData, "UTF-8");
} catch (Exception e) {
Log.e(TAG, "Decryption failed, returning original text", e);
// Return original text if decryption fails - might be unencrypted legacy data
return encryptedText;
}
}
/**
* Get or create the secret key in Android Keystore
*/
private static SecretKey getOrCreateSecretKey() throws Exception {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
if (!keyStore.containsAlias(KEY_ALIAS)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setRandomizedEncryptionRequired(true)
.build();
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
}
return ((KeyStore.SecretKeyEntry) keyStore.getEntry(KEY_ALIAS, null)).getSecretKey();
}
/**
* Check if a string appears to be encrypted (Base64 format)
*
* @param text The text to check
* @return true if text appears to be encrypted
*/
public static boolean isEncrypted(String text) {
if (text == null || text.isEmpty()) {
return false;
}
try {
byte[] decoded = Base64.decode(text, Base64.DEFAULT);
// Check if decoded length is reasonable for encrypted data (IV + data + tag)
return decoded.length > GCM_IV_LENGTH + GCM_TAG_LENGTH;
} catch (Exception e) {
return false;
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="colorPrimary" format="color" />
</resources>
\ No newline at end of file