Skip to content

Commit

Permalink
Apollo: Release source code for 49.4
Browse files Browse the repository at this point in the history
  • Loading branch information
acrespo committed May 23, 2022
1 parent 2ddddd1 commit cc0e92c
Show file tree
Hide file tree
Showing 37 changed files with 347 additions and 178 deletions.
11 changes: 11 additions & 0 deletions android/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,21 @@ follow [https://changelog.md/](https://changelog.md/) guidelines.

## [Unreleased]

## [49.4] - 2022-05-23

### CHANGED
- Receiving Node cell behavior in Operation Detail. Instead of opening 1ML site (private nodes
return a 404 not found), we copy node public key to clipboard.

### FIXED
- A bug that triggered LN invoice regeneration multiple times when a payment was received
- Added a workaround for a strange BadPaddingException coming from Android's Keystore

## [49.3] - 2022-04-26

### ADDED
- Better error reporting and extra metadata for MoneyDecoration (MuunAmountInput) crash
- Show LN alias for outgoing payments in payment history and payment Detail

### FIXED
- Added missing error metadata to some crashlytics errors (e.g for background task of anon users)
Expand Down
2 changes: 1 addition & 1 deletion android/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM openjdk:17-jdk-buster@sha256:9217da81dcff19e60861791511ce57c019e47eaf5ca40d
ENV NDK_VERSION 22.0.7026061
ENV ANDROID_PLATFORM_VERSION 28
ENV ANDROID_BUILD_TOOLS_VERSION 28.0.3
ENV GO_VERSION 1.16.8
ENV GO_VERSION 1.18.1

RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
Expand Down
17 changes: 0 additions & 17 deletions android/apollo/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,6 @@ apply from: "${project.rootDir}/linters/pmd/check-android.gradle"
// https://github.com/spotbugs/spotbugs-gradle-plugin/issues/90
//apply from: "${project.rootDir}/linters/findbugs/check-android.gradle"

/**
* Returns the ip of the local network specified by the TESTING_LOCAL_NETWORK env var.
*/
def testingLocalIp() {
def interfaceName = System.getenv("TESTING_LOCAL_NETWORK")?.trim()

return NetworkInterface.getNetworkInterfaces().
findAll { interfaceName && interfaceName.equalsIgnoreCase(it.displayName) }.
collect { Collections.list it.getInetAddresses() }.
flatten().
find()?. // first or null
getHostAddress()
}

android {
compileSdkVersion 30

Expand All @@ -43,9 +29,6 @@ android {
versionCode 1
versionName "1.0"

// circle
buildConfigField("String", "TESTING_LOCAL_IP", "\"${testingLocalIp()}\"")

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

Expand Down
2 changes: 2 additions & 0 deletions android/apollo/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package io.muun.apollo.data.logging

import com.google.firebase.crashlytics.FirebaseCrashlytics
import io.muun.apollo.domain.action.debug.ForceCrashReportAction
import io.muun.apollo.domain.errors.CyclicalSwapError
import io.muun.apollo.domain.errors.FcmTokenNotAvailableError
import io.muun.apollo.domain.errors.InvoiceAlreadyUsedException
import io.muun.apollo.domain.errors.InvoiceExpiredException
import io.muun.apollo.domain.errors.InvoiceExpiresTooSoonException
import io.muun.apollo.domain.errors.InvoiceMissingAmountException
import io.muun.apollo.domain.errors.NoPaymentRouteException
import io.muun.apollo.domain.errors.UnreachableNodeException
import io.muun.apollo.domain.model.report.CrashReport
import io.muun.apollo.domain.utils.isInstanceOrIsCausedByError

object Crashlytics {

private val crashlytics = FirebaseCrashlytics.getInstance()

/**
* Set up Crashlytics metadata.
*/
@JvmStatic
fun configure(email: String?, userId: String) {
crashlytics.setUserId(userId)
crashlytics.setCustomKey("email", email ?: "unknown")

// TODO: use setUserEmail, and grab Houston session UUID to attach it
}

/**
* Add custom log event, to be displayed under Logs tab. Also queryable via Bigquery. See:
* https://firebase.google.com/docs/crashlytics/customize-crash-reports?platform=android#add-logs
*/
@JvmStatic
fun logBreadcrumb(breadcrumb: String) {
crashlytics.log(breadcrumb);
}

/**
* Send the error to Crashlytics, attaching metadata as key-values with their SDK.
*/
fun reportError(report: CrashReport) {

// Silence some common "nothing to worry about" errors
if (isOnCrashlyticsBlacklist(report.originalError)) {
return
}

crashlytics.setCustomKey("tag", report.tag)
crashlytics.setCustomKey("message", report.message)
crashlytics.setCustomKey("locale", LoggingContext.locale)

for (entry in report.metadata.entries) {
crashlytics.setCustomKey(entry.key, entry.value.toString())
}

crashlytics.recordException(report.error)
}

/**
* Send a "fallback" reporting error to Crashlytics. This means that there was an error while
* doing our usual error report processing. Hence we try to report the original error data (tag,
* message, error) and the error that happened while reporting.
*/
fun reportReportingError(
tag: String?,
message: String?,
originalError: Throwable?,
crashReportingError: Throwable,
) {

tag?.let { crashlytics.setCustomKey("tag", it) }
message?.let { crashlytics.setCustomKey("message", it) }

if (originalError != null) {
crashlytics.recordException(originalError)
}

crashlytics.recordException(crashReportingError)
}

/**
* Forcefully report an exception, bypassing our usual error processing. Meant to only be used
* by ForceCrashReportAction.
*
* @see ForceCrashReportAction
*/
fun forceReport(error: ForceCrashReportAction.ForcedCrashlyticsCall) {
crashlytics.recordException(error)
}

/**
* There are certain errors that are expected and/or there's nothing we can do about it (besides
* properly informing the user about the situation), so let's try to reduce crashlytics noise by
* silencing some common "nothing to worry about" errors.
*/
private fun isOnCrashlyticsBlacklist(error: Throwable?): Boolean {

// If root error has no throwable cause then there's nothing to blacklist
// This is an ugly signature and behaviour to have but makes life easier for caller
if (error == null) {
return false
}

return when {
error.isInstanceOrIsCausedByError<UnreachableNodeException>() -> true
error.isInstanceOrIsCausedByError<NoPaymentRouteException>() -> true
error.isInstanceOrIsCausedByError<InvoiceExpiredException>() -> true
error.isInstanceOrIsCausedByError<InvoiceExpiresTooSoonException>() -> true
error.isInstanceOrIsCausedByError<InvoiceAlreadyUsedException>() -> true
error.isInstanceOrIsCausedByError<InvoiceMissingAmountException>() -> true
error.isInstanceOrIsCausedByError<CyclicalSwapError>() -> true

error.isInstanceOrIsCausedByError<FcmTokenNotAvailableError>() -> true

else -> false
}
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
package io.muun.apollo.data.logging

import com.google.firebase.crashlytics.FirebaseCrashlytics


object LoggingContext {

/**
* Log errors to the Crashlytics.
*/
@JvmStatic
var sendToCrashlytics = true // default for production

/**
* Log errors to the system logs.
*/
@JvmStatic
var sendToLogcat = false // default for production

@JvmStatic
var locale: String = "UNSET" // easily track and attach user's locale to muun errors and reports

@JvmStatic
fun configure(email: String?, userId: String) {
val crashlytics = FirebaseCrashlytics.getInstance()
crashlytics.setUserId(userId)
crashlytics.setCustomKey("email", email ?: "unknown")

// TODO: use setUserEmail, and grab Houston session UUID to attach it
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package io.muun.apollo.data.logging

import android.util.Log
import com.google.firebase.crashlytics.FirebaseCrashlytics
import io.muun.apollo.domain.errors.*
import io.muun.apollo.domain.model.report.CrashReport
import io.muun.apollo.domain.model.report.CrashReportBuilder
import io.muun.apollo.domain.utils.isInstanceOrIsCausedByError
import timber.log.Timber

class MuunTree: Timber.DebugTree() {
class MuunTree : Timber.DebugTree() {

/**
* Log a message, taking steps to enrich and report errors.
Expand All @@ -35,85 +31,29 @@ class MuunTree: Timber.DebugTree() {
private fun sendPreparedCrashReport(tag: String?, message: String?, error: Throwable?) {
val report = CrashReportBuilder.build(tag, message, error)

if (LoggingContext.sendToCrashlytics && !isOnCrashlyticsBlacklist(error)) {
sendToCrashlytics(report)
if (LoggingContext.sendToCrashlytics) {
Crashlytics.reportError(report)
}

if (LoggingContext.sendToLogcat) {
sendToLogcat(report)
Log.e(report.tag, "${report.message} ${report.metadata}", report.error)
}
}

private fun sendFallbackCrashReport(tag: String?,
message: String?,
error: Throwable?,
crashReportingError: Throwable) {
private fun sendFallbackCrashReport(
tag: String?,
message: String?,
error: Throwable?,
crashReportingError: Throwable,
) {

if (LoggingContext.sendToLogcat) {
Log.e("CrashReport:$tag", "During error processing", crashReportingError)
Log.e("CrashReport:$tag", message, error)
}

if (LoggingContext.sendToCrashlytics) {

val crashlytics = FirebaseCrashlytics.getInstance()

if (error != null) {
crashlytics.recordException(error)
}
crashlytics.recordException(crashReportingError)
}
}

/**
* Send the error to the system logs.
*/
private fun sendToLogcat(report: CrashReport) {
Log.e(report.tag, "${report.message} ${report.metadata}", report.error)
}

/**
* Send the error to Crashlytics, attaching metadata as key-values with their SDK.
*/
private fun sendToCrashlytics(report: CrashReport) {
val crashlytics = FirebaseCrashlytics.getInstance()

crashlytics.setCustomKey("tag", report.tag)
crashlytics.setCustomKey("message", report.message)
crashlytics.setCustomKey("locale", LoggingContext.locale)

for (entry in report.metadata.entries) {
crashlytics.setCustomKey(entry.key, entry.value.toString())
}

crashlytics.recordException(report.error)
}

/**
* There are certain errors that are expected and/or there's nothing we can do about it (besides
* properly informing the user about the situation), so let's try to reduce crashlytics noise by
* silencing some common "nothing to worry about" errors.
*/
private fun isOnCrashlyticsBlacklist(error: Throwable?): Boolean {

// If root error has no throwable cause then there's nothing to blacklist
// This is an ugly signature and behaviour to have but makes life easier for caller
if (error == null) {
return false
}

return when {
error.isInstanceOrIsCausedByError<UnreachableNodeException>() -> true
error.isInstanceOrIsCausedByError<NoPaymentRouteException>() -> true
error.isInstanceOrIsCausedByError<InvoiceExpiredException>() -> true
error.isInstanceOrIsCausedByError<InvoiceExpiresTooSoonException>() -> true
error.isInstanceOrIsCausedByError<InvoiceAlreadyUsedException>() -> true
error.isInstanceOrIsCausedByError<InvoiceMissingAmountException>() -> true
error.isInstanceOrIsCausedByError<CyclicalSwapError>() -> true

error.isInstanceOrIsCausedByError<FcmTokenNotAvailableError>() -> true

else -> false
Crashlytics.reportReportingError(tag, message, error, crashReportingError)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.muun.apollo.data.os.secure_storage;

import io.muun.apollo.domain.errors.SecureStorageError;
import io.muun.common.utils.Encodings;
import io.muun.common.utils.Preconditions;

import rx.Observable;
Expand Down Expand Up @@ -39,7 +40,9 @@ public byte[] get(String key) {
return retrieveDecrypted(key);

} catch (Throwable e) {
throw new SecureStorageError(e, debugSnapshot());
final SecureStorageError ssError = new SecureStorageError(e, debugSnapshot());
enhanceError(ssError, key);
throw ssError;
}
}

Expand All @@ -60,7 +63,9 @@ public void put(String key, byte[] value) {
storeEncrypted(key, value);

} catch (Throwable e) {
throw new SecureStorageError(e, debugSnapshot());
final SecureStorageError ssError = new SecureStorageError(e, debugSnapshot());
enhanceError(ssError, key);
throw ssError;
}

preferences.recordAuditTrail("PUT", key);
Expand Down Expand Up @@ -147,11 +152,17 @@ private void storeEncrypted(String key, byte[] input) {
preferences.saveBytes(keyStore.encryptData(input, key, preferences.getAesIv(key)), key);
}

private void enhanceError(SecureStorageError ssError, String key) {
ssError.addMetadata("key", key);
ssError.addMetadata("cypherText", Encodings.bytesToHex(preferences.getBytes(key)));
ssError.addMetadata("aesIV", Encodings.bytesToHex(preferences.getAesIv(key)));
}

/**
* Take a debug snapshot of the current state of the secure storage. This is safe to
* report without compromising any user data.
*/
public DebugSnapshot debugSnapshot() {
private DebugSnapshot debugSnapshot() {
// NEVER ever return any values from the keystore itself, only labels should get out.

Set<String> keystoreLabels = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ public void storePublicChallengeKey(ChallengePublicKey publicKey, ChallengeType
}

secureStorageProvider.put(
KEY_CHALLENGE_PUBLIC_KEY + type.toString(),
KEY_CHALLENGE_PUBLIC_KEY + type,
publicKey.serialize()
);

Expand Down
Loading

0 comments on commit cc0e92c

Please sign in to comment.