JMP

XMPPTwitterReddit
Featured Image

Newsletter: Year in Review, Google Play Update

singpolyma@singpolyma.net

Hi everyone!

Welcome to the latest edition of your pseudo-monthly JMP update!

In case it’s been a while since you checked out JMP, here’s a refresher: JMP lets you send and receive text and picture messages (and calls) through a real phone number right from your computer, tablet, phone, or anything else that has a Jabber client.  Among other things, JMP has these features: Your phone number on every device; Multiple phone numbers, one app; Free as in Freedom; Share one number with multiple people.

As we approach the close of 2024, we want to take a moment to reflect on a year full of growth, innovation, and connection. Thanks to your support and engagement, JMP has continued to thrive as a service that empowers you to stay connected with the world using open standards and flexible technology. Here’s a look back at some of the highlights that made this year so special:

Cheogram Android

Cheogram Android, which we sponsor, experienced significant developments this year. Besides the preferred distribution channel of F-Droid, the app is also available on other platforms like Aptoide and the Amazon Appstore. It was removed from the Google Play Store in September for unknown reasons, and after a long negotiation has been restored to Google Play without modification.

Cheogram Android saw several exciting feature updates this year, including:

  • Major visual refresh
  • Animated custom emoji
  • Better Reactions UI (including custom emoji reactions)
  • Widgets powered by WebXDC for interactive chats and app extensions
  • Initial support for link previews
  • The addition of a navigation drawer to show chats from only one account or tag
  • Allowing edits to any message you have sent

This month also saw the release of 2.17.2-3 including:

  • Fix direct shares on Android 12+
  • Option to hide media from gallery
  • Do not re-notify dismissed notifications
  • Experimental extensions support based on WebXDC
  • Experimental XEP-0227 export support

Of course nothing in Cheogram Android would be possible without the hard work of the upstream project, Conversations, so thanks go out to the devs there as well.

eSIM Adapter Launch

This year, we introduced the JMP eSIM Adapter—a device that bridges the gap for devices without native eSIM support, and adds flexibility for devices with eSIM support. Whether you’re travelling, upgrading your device, or simply exploring new options, the eSIM Adapter makes it seamless to transfer eSIMs across your devices.

Engaging with the Community

This year, we hosted booths at SeaGL, FOSSY, and HOPE, connecting with all of you in person. These booths provided opportunities to learn about our services, pay for subscriptions, or purchase eSIM Adapters face-to-face.

Addressing Challenges

In 2024, we also tackled some pressing industry issues, such as SMS censorship. To help users avoid censorship and gain access to bigger MMS group chats, we’ve added new routes that you can request from our support team.

As part of this, we also rolled out the ability for JMP customers to receive calls directly over SIP.

Holiday Support Schedule

We want to inform you that JMP support will be reduced from our usual response level from December 23 until January 6. During this period, response times will be significantly longer than usual as our support staff take time with their families. We appreciate your understanding and patience.

Looking Ahead

As we move into 2025, we’re excited to keep building on this momentum. Expect even more features, improved services, and expanded opportunities to connect with the JMP community. Your feedback has been, and will always be, instrumental in shaping the future of JMP.

To learn what’s happening with JMP between newsletters, here are some ways you can find out:

Featured Image

Verify Google Play App Purchase on Your Server

singpolyma@singpolyma.net

We are preparing for the first-ever Google Play Store launch of Cheogram Android as part of JMP coming out of beta later this year.  One of the things we wanted to “just work” for Google Play users is to be able to pay for the app and get their first month of JMP “bundled” into that purchase price, to smooth the common onboarding experience.  So how do the JMP servers know that the app communicating with them is running a version of the app bought from Google Play as opposed to our builds, F-Droid’s builds, or someone’s own builds?  And also ensure that this person hasn’t already got a bundled month before?  The documentation available on how to do this is surprisingly sparse, so let’s do this together.

Client Side

Google publishes an official Licensing Verification Library for communicating with Google Play from inside an Android app to determine if this install of the app can be associated with a Google Play purchase.  Most existing documentation focuses on using this library, however it does not expose anything in the callbacks other than “yes license verified” or “no, not verified”.  This can allow an app to check if it is a purchased copy itself, but is not so useful for communicating that proof onward to a server.  The library also contains some exciting snippets like:

// Base64 encoded -
// com.android.vending.licensing.ILicensingService
// Consider encoding this in another way in your
// code to imp rove security
Base64.decode(
    "Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")))

Which implies that they expect developers to fork this code to use it.  Digging in to the code we find in LicenseValidator.java:

public void verify(PublicKey publicKey, int responseCode, String signedData, String signature)

Which looks like exactly what we need: the actual signed assertion from Google Play and the signature!  So we just need a small patch to pass those along to the callback as well as the response code currently being passed.  Then we can use the excellent jitpack to include the forked library in our app:

implementation 'com.github.singpolyma:play-licensing:1c637ea03c'

Then we write a small class in our app code to actually use it:

import android.content.Context;
import com.google.android.vending.licensing.*;
import java.util.function.BiConsumer;

public class CheogramLicenseChecker implements LicenseCheckerCallback {
    private final LicenseChecker mChecker;
    private final BiConsumer mCallback;

    public CheogramLicenseChecker(Context context, BiConsumer<String, String> callback) {
        mChecker = new LicenseChecker(  
            context,  
            new StrictPolicy(), // Want to get a signed item every time  
            context.getResources().getString(R.string.licensePublicKey)  
        );
        mCallback = callback;
    }

    public void checkLicense() {
        mChecker.checkAccess(this);
    }

    @Override
    public void dontAllow(int reason) {
        mCallback.accept(null, null);
    }

    @Override
    public void applicationError(int errorCode) {
        mCallback.accept(null, null);
    }

    @Override
    public void allow(int reason, ResponseData data, String signedData, String signature) {
        mCallback.accept(signedData, signature);
    }
}

Here we use the StrictPolicy from the License Verification Library because we want to get a fresh signed data every time, and if the device is offline the whole question is moot because we won’t be able to contact the server anyway.

This code assumes you put the Base64 encoded licensing public key from “Monetisation Setup” in Play Console into a resource R.string.licensePublicKey.

Then we need to communicate this to the server, which you can do whatever way makes sense for your protocol; with XMPP we can easily add custom elements to our existing requests so:

new com.cheogram.android.CheogramLicenseChecker(context, (signedData, signature) -> {
    if (signedData != null && signature != null) {
        c.addChild("license", "https://ns.cheogram.com/google-play").setContent(signedData);
        c.addChild("licenseSignature", "https://ns.cheogram.com/google-play").setContent(signature);
    }

    xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
        session.updateWithResponse(iq);
    });
}).checkLicense();

Server Side

When trying to verify this on the server side we quickly run into some new issues.  What format is this public key in?  It just says “public key” and is Base64 but that’s about it.  What signature algorithm is used for the signed data?  What is the format of the data itself?  Back to the library code!

private static final String KEY_FACTORY_ALGORITHM = "RSA";
…
byte[] decodedKey = Base64.decode(encodedPublicKey);
…
new X509EncodedKeySpec(decodedKey)

So we can see it is an X509 related encoded, and indeed turns out to be Base64 encoded DER.  So we can run this:

echo "BASE64_STRING" | base64 -d | openssl rsa -pubin -inform der -in - -text

to get the raw properties we might need for any library (key size, modulus, and exponent).  Of course, if your library supports parsing DER directly you can also use that.

import java.security.Signature;
…
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
…
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());

Combined with the java documentation we can thus say that the signature algoritm is PKCS#1 padded RSA with SHA1.

And finally:

String[] fields = TextUtils.split(mainData, Pattern.quote("|"));
data.responseCode = Integer.parseInt(fields[0]);
data.nonce = Integer.parseInt(fields[1]);
data.packageName = fields[2];
data.versionCode = fields[3];
// Application-specific user identifier.
data.userId = fields[4];
data.timestamp = Long.parseLong(fields[5]);

The format of the data, pipe-seperated text. The main field of interest for us is userId which is (as it says in a comment) “a user identifier unique to the <application, user> pair”. So in our server code:

import Control.Error (atZ)
import qualified Data.ByteString.Base64 as Base64
import qualified Data.Text as T
import Crypto.Hash.Algorithms (SHA1(SHA1))
import qualified Crypto.PubKey.RSA as RSA
import qualified Crypto.PubKey.RSA.PKCS15 as RSA
import qualified Data.XML.Types as XML

googlePlayUserId
    | googlePlayVerified = (T.split (=='|') googlePlayLicense) `atZ` 4
    | otherwise = Nothing
googlePlayVerified = fromMaybe False $ fmap (\pubKey ->
    RSA.verify (Just SHA1) pubKey (encodeUtf8 googlePlayLicense)
        (Base64.decodeLenient $ encodeUtf8 googlePlaySig)
    ) googlePlayPublicKey
googlePlayLicense = mconcat $ XML.elementText
    =<< XML.isNamed (s"{https://ns.cheogram.com/google-play}license")
    =<< XML.elementChildren payload
googlePlaySig = mconcat $ XML.elementText
    =<< XML.isNamed (s"{https://ns.cheogram.com/google-play}licenseSignature")
    =<< XML.elementChildren payload

We can then use the verified and extracted googlePlayUserId value to check if this user has got a bundled month before and, if not, to provide them with one during signup.

Creative Commons Attribution ShareAlike