I am trying to verify the phone number of an Android device by having the device send an SMS to itself, and automatically checking if the SMS has been received. How can I do this?
When you send a message with Verified SMS, both your agent and the user's device hash the message content with a shared secret (generated with each others' keys) and send the message hash to Verified SMS. Verified SMS confirms that the message hashes match and notifies the Messages app that the message is verified.
For authenticating sign-ins and transactions on mobile apps, SMS-based verification has emerged as a de-facto standard: you receive a text message with an OTP (one-time password), confirming that you are the phone's owner. This method of mobile authentication, however, isn't actually as secure as you may think.
To begin, this will require two permissions; one to send SMS messages, and one to receive them. The following needs to be in your AndroidManifest.xml, between the <manifest>
tags, but outside of the <application>
tags.
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
These are both dangerous permissions, so you will need to handle them accordingly if your app is to run on Marshmallow (API level 23) or above, and has a targetSdkVersion
of 23+. Information on how to request these permissions at runtime can be found on this developer page.
The Java classes you will need are in the android.telephony
package; specifically android.telephony.SmsManager
and android.telephony.SmsMessage
. Do make certain you've got the correct classes imported for both.
To send the outgoing SMS, you will use SmsManager
's sendTextMessage()
method, which has the following signature:
sendTextMessage(String destinationAddress, String scAddress, String text,
PendingIntent sentIntent, PendingIntent deliveryIntent)
Only two arguments are required in this method call - destinationAddress
and text
; the first being the phone number, the second being the message content. null
can be passed for the rest. For example:
String number = "1234567890";
String message = "Verification message.";
SmsManager sm = SmsManager.getDefault();
sm.sendTextMessage(number, null, message, null, null);
It's important to keep the message text relatively short, as sendTextMessage()
will usually fail silently if the text length exceeds the character limit for a single message.
To receive and read the incoming message, you will need to register a BroadcastReceiver
with an IntentFilter
for the "android.provider.Telephony.SMS_RECEIVED"
action. This Receiver can be registered either statically in the manifest, or dynamically on a Context
at runtime.
Statically registering the Receiver class in the manifest will allow your app to receive the incoming message even if your app should happen to be killed before receipt. It may, however, take a little extra work to get the results where you want them. Between the <application>
tags:
<receiver
android:name=".SmsReceiver"
android:enabled="false">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
The PackageManager#setComponentEnabledSetting()
method can be used to enable and disable this <receiver>
as needed.
Dynamically registering a Receiver instance on a Context
can be a little easier to manage, code-wise, as the Receiver class could be made an inner class on whichever component registers it, and therefore have direct access to that component's members. However, this approach might not be as reliable as static registration, as a few different things could prevent the Receiver from getting the broadcast; e.g., your app's process being killed, the user navigating away from the registering Activity
, etc.
SmsReceiver receiver = new SmsReceiver();
IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
registerReceiver(receiver, filter);
Do remember to unregister the Receiver when appropriate.
In the Receiver's onReceive()
method, the actual message comes as an array of byte
arrays attached to the Intent
as an extra. The decoding details vary depending on the Android version, but the result here is a single SmsMessage
object that will have the phone number and message you're after.
class SmsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
SmsMessage msg;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
SmsMessage[] msgs = Telephony.Sms.Intents.getMessagesFromIntent(intent);
msg = msgs[0];
} else {
Object pdus[] = (Object[]) intent.getExtras().get("pdus");
msg = SmsMessage.createFromPdu((byte[]) pdus[0]);
}
String number = msg.getOriginatingAddress();
String message = msg.getMessageBody();
...
}
}
At this point, you simply compare the number
here to the one passed to the sendTextMessage()
call. It's advisable to use PhoneNumberUtils.compare()
for this, since the number retrieved in the Receiver might be in a different format than the one addressed.
Notes:
The example demonstrated here is using one single-part message, thus why the message text should be restricted to a relatively short length. If you do want to send a longer message, for some reason, the sendMultipartTextMessage()
method can be used instead. You would need to split up the text first, using SmsManager#divideMessage()
, and passing the resulting ArrayList
to that method, in lieu of the message String
. To reassemble the complete message in the Receiver, you'd have to decode each byte[]
into an SmsMessage
, and concatenate the message bodies.
Since KitKat (API level 19), if your app is not the default messaging app, the messages used here are going to be saved to the SMS Provider by the system and default app, and will therefore be available to any other app that uses the Provider. There's not much you can do about that, but if you really want to avoid it, this same technique can be used with data SMS, which do not trigger the default app, and won't be saved to the Provider.
For this, the sendDataMessage()
method is used, which will need an additional short
argument for the (arbitrary) port number, and the message is passed as a byte[]
, rather than a String
. The action to filter for is "android.intent.action.DATA_SMS_RECEIVED"
, and the filter will need a data scheme and authority (host and port) set. In the manifest, it would look like:
<intent-filter>
<action android:name="android.intent.action.DATA_SMS_RECEIVED" />
<data
android:scheme="sms"
android:host="localhost"
android:port="1234" />
</intent-filter>
and there are corresponding methods in the IntentFilter
class to set those dynamically.
Decoding the SmsMessage
is the same, but the message byte[]
is retrieved with getUserData()
, rather than getMessageBody()
.
Prior to KitKat, apps were responsible for writing their own outgoing messages, so you can just not do that on those versions, if you don't want any record of it.
Incoming messages could be intercepted, and their broadcasts aborted before the main messaging app could receive and write them. To accomplish this, the filter's priority is set to the maximum, and abortBroadcast()
is called in the Receiver. In the static option, the android:priority="999"
attribute is added to the opening <intent-filter>
tag. Dynamically, the IntentFilter#setPriority()
method can do the same.
This is not at all reliable, as it is always possible for another app to have a higher precedence than yours.
I've omitted securing the Receiver with the broadcaster's permission in these examples, partly for simplicity and clarity, and partly because the nature of the thing wouldn't really leave you open to any sort of spoofing that could do harm. However, if you'd like to include this, then you merely need to add the android:permission="android.permission.BROADCAST_SMS"
attribute to the opening <receiver>
tag for the static option. For the dynamic, use the four-parameter overload of the registerReceiver()
method, passing that permission String
as the third argument, and null
as the fourth.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With