I'm reading about certificate pinning on Android and I'm confused. I'm not using okhttp or retrofit so I have to do it manually.
There is a tutorial here: https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning#Android
where they are adding the certificate to list of trusted certificates. But there is also another tutorial when we're checking base64 of sha256 of the certificate installed on the server: https://medium.com/#appmattus/android-security-ssl-pinning-1db8acb6621e
Which approach is the correct one?
Why can't we just receive sha256 from the server in the header as browsers do and store it somewhere?
I would recommend this
https://www.paypal-engineering.com/2015/10/14/key-pinning-in-mobile-applications/
Android Method
The simplest approach is to use a JSEE-based method as shown below. This is the recommended approach for Android. The method’s input arguments are an HTTPS connection and a set of valid pins for the targeted URL.
private boolean validatePinning(HttpsURLConnection conn, Set<String> validPins) {
try {
Certificate[] certs = conn.getServerCertificates();
MessageDigest md = MessageDigest.getInstance("SHA-256");
for (Certificate cert : certs) {
X509Certificate x509Certificate = (X509Certificate) cert;
byte[] key = x509Certificate.getPublicKey().getEncoded();
md.update(key, 0, key.length);
byte[] hashBytes = md.digest();
StringBuffer hexHash = new StringBuffer();
for (int i = 0; i < hashBytes.length; i++) {
int k = 0xFF & hashBytes[i];
String tmp = (k<16)? "0" : "";
tmp += Integer.toHexString(0xFF & hashBytes[i]);
hexHash.append(tmp);
}
if (validPins.contains(hexHash.toString())) {
return true;
}
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return false;
}
The pins are declared as strings. For instance:
Declaring Key Pins
private static final Set<String> PINS = new HashSet<String>(Arrays.asList(
new String[]{
"996b510ce2380da9c738...87cb13c9ec409941",
"ba47e83b1ccf0939bb40d2...edf856ba892c06481a"}));
Leveraging the above method, here is an example showing how this can be put to use. The only relevant portion is highlighted below.
Example Using Key Pinning
protected String doInBackground(String... urls) {
try {
/** Test pinning given the target URL **/
/** for now use pre-defined endpoint URL instead or urls[0] **/
Log.i(LOG_TAG, "==> PinningTestTask launched.");
String dest = defaultEndpoint;
URL targetURL = new URL(dest);
HttpsURLConnection targetConnection = (HttpsURLConnection) targetURL.openConnection();
targetConnection.connect();
if (validatePinning(targetConnection, PINS)) {
final String updateText = "Key pinning succeded for: " + dest;
runOnUiThread(new Runnable() {
#Override
public void run() {
textView.setText(updateText);
}
});
} else {
final String updateText = "Key pinning failed for: " + dest;
runOnUiThread(new Runnable() {
#Override
public void run() {
textView.setText(updateText);
}
});
}
} catch (Exception e) {
e.printStackTrace();
final String updateText = "Key pinning failed for: " + dest + "\n" + e.toString();
runOnUiThread(new Runnable() {
#Override
public void run() {
textView.setText(updateText);
}
});
}
return null;
}
Related
I need to set up a JITR (Just in time registration) process to AWS IOT core for my Android Application using Android AWS IOT SDK. My implementation is done referring this . However, When trying to create a thing after creating a fresh X.509 certificate (On the first connection attempt), Even though the certificate is created on the server, the Server returns errors regarding "unmatched signatures and an empty template body".
This is my current sample implementation,
public class MainActivity extends AppCompatActivity {
private boolean thingExist;
private String certificateARN;
// IoT endpoint
// AWS Iot CLI describe-endpoint call returns: XXXXXXXXXX.iot.<region>.amazonaws.com
private static final String CUSTOMER_SPECIFIC_ENDPOINT = "";
// Cognito pool ID. For this app, pool needs to be unauthenticated pool with
// AWS IoT permissions.
private static final String COGNITO_POOL_ID = "";
// Name of the AWS IoT policy to attach to a newly created certificate
private static final String AWS_IOT_POLICY_NAME = "";
// Region of AWS IoT
private static final Regions MY_REGION = Regions.US_EAST_2;
// Filename of KeyStore file on the filesystem
private static final String KEYSTORE_NAME = "iot_keystore";
// Password for the private key in the KeyStore
private static final String KEYSTORE_PASSWORD = "password";
// Certificate and key aliases in the KeyStore
private static final String CERTIFICATE_ID = "default";
AWSIotClient mIotAndroidClient;
AWSIotMqttManager mqttManager;
String clientId;
String keystorePath;
String keystoreName;
String keystorePassword;
private boolean isAWSConnected;
private listThings mTask;
private boolean certificateExist;
private final String LOG_TAG = "AWS";
private final String serialId = "1110";
KeyStore clientKeyStore = null;
String certificateId;
CognitoCachingCredentialsProvider credentialsProvider;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
clientId = Settings.Global.getString(getApplicationContext().getContentResolver(), "device_name");
// Initialize the AWS Cognito credentials provider
credentialsProvider = new CognitoCachingCredentialsProvider(
getApplicationContext(), // context
COGNITO_POOL_ID, // Identity Pool ID
MY_REGION // Region
);
Region region = Region.getRegion(MY_REGION);
// MQTT Client
mqttManager = new AWSIotMqttManager(clientId, CUSTOMER_SPECIFIC_ENDPOINT);
// Set keepalive to 10 seconds. Will recognize disconnects more quickly but will also send
// MQTT pings every 10 seconds.
mqttManager.setKeepAlive(10);
// Set Last Will and Testament for MQTT. On an unclean disconnect (loss of connection)
// AWS IoT will publish this message to alert other clients.
AWSIotMqttLastWillAndTestament lwt = new AWSIotMqttLastWillAndTestament("Last will",
new Gson().toJson("Android client lost connection"), AWSIotMqttQos.QOS0);
mqttManager.setMqttLastWillAndTestament(lwt);
mIotAndroidClient = new AWSIotClient(credentialsProvider);
mIotAndroidClient.setRegion(region);
keystorePath = getFilesDir().getPath();
keystoreName = KEYSTORE_NAME;
keystorePassword = KEYSTORE_PASSWORD;
certificateId = CERTIFICATE_ID;
// To load cert/key from keystore on filesystem
try {
if (AWSIotKeystoreHelper.isKeystorePresent(keystorePath, keystoreName)) {
if (AWSIotKeystoreHelper.keystoreContainsAlias(certificateId, keystorePath,
keystoreName, keystorePassword)) {
Log.i(LOG_TAG, "Certificate " + certificateId
+ " found in keystore - using for MQTT.");
// load keystore from file into memory to pass on connection
clientKeyStore = AWSIotKeystoreHelper.getIotKeystore(certificateId,
keystorePath, keystoreName, keystorePassword);
certificateExist = true;
mTask = (listThings) new listThings().execute();
} else {
Log.i(LOG_TAG, "Key/cert " + certificateId + " not found in keystore.");
}
} else {
Log.i(LOG_TAG, "Keystore " + keystorePath + "/" + keystoreName + " not found.");
}
} catch (Exception e) {
Log.e(LOG_TAG, "An error occurred retrieving cert/key from keystore.", e);
}
if (clientKeyStore == null) {
certificateExist = false;
Log.i(LOG_TAG, "Cert/key was not found in keystore - creating new key and certificate.");
new Thread(new Runnable() {
#Override
public void run() {
try {
// Create a new private key and certificate. This call
// creates both on the server and returns them to the
// device.
CreateKeysAndCertificateRequest createKeysAndCertificateRequest =
new CreateKeysAndCertificateRequest();
createKeysAndCertificateRequest.setSetAsActive(true);
final CreateKeysAndCertificateResult createKeysAndCertificateResult;
createKeysAndCertificateResult =
mIotAndroidClient.createKeysAndCertificate(createKeysAndCertificateRequest);
Log.i(LOG_TAG,
"Cert ID: " +
createKeysAndCertificateResult.getCertificateId() +
" created.");
// store in keystore for use in MQTT client
// saved as alias "default" so a new certificate isn't
// generated each run of this application
AWSIotKeystoreHelper.saveCertificateAndPrivateKey(certificateId,
createKeysAndCertificateResult.getCertificatePem(),
createKeysAndCertificateResult.getKeyPair().getPrivateKey(),
keystorePath, keystoreName, keystorePassword);
certificateARN = createKeysAndCertificateResult.getCertificateArn();
// load keystore from file into memory to pass on
// connection
clientKeyStore = AWSIotKeystoreHelper.getIotKeystore(certificateId,
keystorePath, keystoreName, keystorePassword);
// Attach a policy to the newly created certificate.
// This flow assumes the policy was already created in
// AWS IoT and we are now just attaching it to the
// certificate.
AttachPrincipalPolicyRequest policyAttachRequest =
new AttachPrincipalPolicyRequest();
policyAttachRequest.setPolicyName(AWS_IOT_POLICY_NAME);
policyAttachRequest.setPrincipal(createKeysAndCertificateResult
.getCertificateArn());
mIotAndroidClient.attachPrincipalPolicy(policyAttachRequest);
mTask = (listThings) new listThings().execute();
} catch (Exception e) {
Log.e(LOG_TAG,
"Exception occurred when generating new private key and certificate.",
e);
}
}
}).start();
}
}
private class listThings extends AsyncTask<Void, Void, Boolean> {
// Listing registered things to verify whether a thing already exist under the same device ID.
#Override
protected Boolean doInBackground(Void... voids) {
Log.d(LOG_TAG, "Listing Things");
final ListThingsRequest request = new ListThingsRequest().withAttributeName("Device_ID")
.withAttributeValue(serialId).withMaxResults(1);
final ListThingsResult result = mIotAndroidClient.listThings(request);
if (!result.getThings().isEmpty()) {
thingExist = true;
return true;
} else {
thingExist = false;
return false;
}
}
#Override
protected void onPostExecute(Boolean aBoolean) {
super.onPostExecute(aBoolean);
Log.d(LOG_TAG, "onPostExecute: " + aBoolean.toString());
if (certificateExist) {
new createThing().execute();
} else {
new awsAsync().execute();
}
mTask.cancel(true);
}
}
private class awsAsync extends AsyncTask<Void, Void, Void> {
#Override
protected Void doInBackground(Void... voids) {
if (thingExist) {
//Thing Exist, certificate doesn't exist
AttachThingPrincipalRequest thingPrincipalRequest = new AttachThingPrincipalRequest();
thingPrincipalRequest.setThingName(serialId);
thingPrincipalRequest.setPrincipal(certificateARN);
Log.d(LOG_TAG, "certificateARN " + certificateARN);
mIotAndroidClient.attachThingPrincipal(thingPrincipalRequest);
/*check if previous certificates are available and delete them*/
} else {
//Thing Doesn't Exist, certificate doesn't exist
try {
Log.d(LOG_TAG, "Thing does not Exist, certificate doesn't exist");
Map<String, String> attributes = new HashMap<String, String>() {{
put("Device_ID", serialId);
put("Android_version", Build.VERSION.RELEASE);
}};
CreateThingRequest createThingRequest = new CreateThingRequest();
createThingRequest.setThingName(serialId);
AttributePayload attributePayload = new AttributePayload();
attributePayload.setAttributes(attributes);
createThingRequest.setThingTypeName("Consenz_search");
createThingRequest.setAttributePayload(attributePayload);
mIotAndroidClient.createThing(createThingRequest);
AddThingToThingGroupRequest addThingToThingGroupRequest = new AddThingToThingGroupRequest();
addThingToThingGroupRequest.setThingName(serialId);
addThingToThingGroupRequest.setThingGroupName("Consenz_Group");
mIotAndroidClient.addThingToThingGroup(addThingToThingGroupRequest);
AttachThingPrincipalRequest thingPrincipalRequest = new AttachThingPrincipalRequest();
thingPrincipalRequest.setThingName(serialId);
thingPrincipalRequest.setPrincipal(certificateARN);
mIotAndroidClient.attachThingPrincipal(thingPrincipalRequest);
} catch (Exception e) {
Log.e(LOG_TAG,
"Excepetion occured when creating thing",
e);
}
}
return null;
}
#Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
connectToAWS();
}
}
private class createThing extends AsyncTask<Void, Void, Void> {
#Override
protected Void doInBackground(Void... voids) {
if (!thingExist) {
try {
//Thing doesn't exist, Certificate exist
Log.d(LOG_TAG, "Thing does not Exist. Creating a thing");
Map<String, String> attributes = new HashMap<String, String>() {{
put("Device_ID", serialId);
put("Android_version", Build.VERSION.RELEASE);
}};
CreateThingRequest createThingRequest = new CreateThingRequest();
createThingRequest.setThingName(serialId);
AttributePayload attributePayload = new AttributePayload();
attributePayload.setAttributes(attributes);
createThingRequest.setThingTypeName("Consenz_search");
createThingRequest.setAttributePayload(attributePayload);
mIotAndroidClient.createThing(createThingRequest);
AddThingToThingGroupRequest addThingToThingGroupRequest = new AddThingToThingGroupRequest();
addThingToThingGroupRequest.setThingName(serialId);
addThingToThingGroupRequest.setThingGroupName("Consenz_Group");
mIotAndroidClient.addThingToThingGroup(addThingToThingGroupRequest);
AttachThingPrincipalRequest thingPrincipalRequest = new AttachThingPrincipalRequest();
thingPrincipalRequest.setThingName(serialId);
thingPrincipalRequest.setPrincipal(AWSIotKeystoreHelper.AWS_IOT_INTERNAL_KEYSTORE_PASSWORD);
mIotAndroidClient.attachThingPrincipal(thingPrincipalRequest);
//need to attach the current thing to existing certificate.
} catch (Exception e) {
Log.e(LOG_TAG,
"Excepetion occured when creating thing",
e);
}
} else {
//Thing exist, Certificate exist
//need to attach the current thing to existing certificate.
Log.d(LOG_TAG, "We here");
final ListThingPrincipalsRequest request = new ListThingPrincipalsRequest().withThingName(serialId);
final ListThingPrincipalsResult result = mIotAndroidClient.listThingPrincipals(request);
DescribeCertificateRequest request1 = new DescribeCertificateRequest().withCertificateId("default");
DescribeCertificateResult result1 = mIotAndroidClient.describeCertificate(request1);
String arn = result1.getCertificateDescription().getCertificateArn();
result.toString();
Log.d(LOG_TAG, "arn " + arn);
Log.d(LOG_TAG, "certificates " + result);
//Log.d(LOG_TAG, "Thing Exists. Attaching thing to certificate");
AttachThingPrincipalRequest thingPrincipalRequest = new AttachThingPrincipalRequest();
thingPrincipalRequest.setThingName(serialId);
//String arn = thingPrincipalRequest.getPrincipal(c);
//Log.d(LOG_TAG, "arn "+arn);
mIotAndroidClient.attachThingPrincipal(thingPrincipalRequest);
}
return null;
}
#Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
//Log.d(LOG_TAG, "Done creating thing");
connectToAWS();
}
}
private void connectToAWS() {
try {
Log.d(LOG_TAG, "Client ID: " + clientId);
mqttManager.connect(clientKeyStore, new AWSIotMqttClientStatusCallback() {
#Override
public void onStatusChanged(final AWSIotMqttClientStatus status,
final Throwable throwable) {
Log.d(LOG_TAG, "Status = " + String.valueOf(status));
runOnUiThread(new Runnable() {
#Override
public void run() {
if (status == AWSIotMqttClientStatus.Connecting) {
Log.d(LOG_TAG, "Connecting...");
isAWSConnected = false;
} else if (status == Connected) {
Log.d(LOG_TAG, "Connected");
isAWSConnected = true;
//creatething
publishToAWS();
} else if (status == AWSIotMqttClientStatus.Reconnecting) {
if (throwable != null) {
Log.e(LOG_TAG, "Connection error.", throwable);
isAWSConnected = false;
}
isAWSConnected = false;
Log.d(LOG_TAG, "Reconnecting");
} else if (status == AWSIotMqttClientStatus.ConnectionLost) {
if (throwable != null) {
Log.e(LOG_TAG, "Connection error.", throwable);
isAWSConnected = false;
}
isAWSConnected = false;
Log.d(LOG_TAG, "Disconnected");
} else {
isAWSConnected = false;
Log.d(LOG_TAG, "Disconnected");
}
}
});
}
});
} catch (final Exception e) {
Log.d(LOG_TAG, "Error! " + e.getMessage());
}
}
private void publishToAWS() {
//publishing MQQT messages to AWS
String version = "";
String versionRelease = Build.VERSION.RELEASE;
try {
version = this.getPackageManager().getPackageInfo(this.getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
try {
mqttManager.publishString(new Gson().toJson(serialId), "Device_ID", AWSIotMqttQos.QOS0);
mqttManager.publishString(new Gson().toJson(version), "APK_Version", AWSIotMqttQos.QOS0);
mqttManager.publishString(new Gson().toJson(versionRelease), "Android_Version", AWSIotMqttQos.QOS0);
} catch (Exception e) {
Log.e(LOG_TAG, "Publish error.", e);
}
}
}
what am I doing wrong here? thanks in advance.
I am new to OpenTelemetry word. I have created spans for my services separately, but when i am try to combine spans of two different services, using context propogation, I am not able to do it successfully.
I have used following code:
// at client side:
public static void sendContext(String resource) {
TextMapSetter<HttpURLConnection> setter =
new TextMapSetter<HttpURLConnection>() {
#Override
public void set(HttpURLConnection carrier, String key, String value) {
carrier.setRequestProperty(key, value);
}
};
HttpURLConnection transportLayer = null;
String urlString = "http://127.0.0.1:8080" + resource;
try {
URL url = new URL(urlString);
transportLayer = (HttpURLConnection) url.openConnection();
} catch (MalformedURLException ex) {
System.out.println(ex.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
}
GlobalOpenTelemetry.getPropagators()
.getTextMapPropagator()
.inject(Context.current(), transportLayer, setter);
}
// at server side:
public static Context getContext(HttpServletRequest request) {
TextMapGetter<HttpServletRequest> getter =
new TextMapGetter<HttpServletRequest>() {
#Override
public String get(HttpServletRequest carrier, String key) {
Enumeration<String> headerNames = carrier.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println("headerNames.nextElement(): " + headerName);
if (headerName.equals(key)) {
String headerValue = request.getHeader(headerName);
System.out.println("headerValue): " + headerValue);
return headerValue;
}
}
}
return null;
}
#Override
public Iterable<String> keys(HttpServletRequest carrier) {
Set<String> set = new HashSet<String>();
Enumeration<String> headerNames = carrier.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
set.add(headerNames.nextElement());
}
}
return set;
}
};
Context extractedContext =
GlobalOpenTelemetry.getPropagators()
.getTextMapPropagator()
.extract(Context.current(), request, getter);
At server, i am not able to get parent span.
Kindly help on this.
You can refer to OpenTelemetry main documentation from here. It contains the context propagation part but I used HttpHeader type getter as the TextMapGetter with the same functionality which shows in the doc and instead of using
Scope scope = extractedContext.makeCurrent()
as the scope to create a child span, better to use directly without the scope,
tracer.spanBuilder(spanName).setParent(extractedContext)
Because sometimes the automated way to propagate the parent span on the current thread does not work fine.
I am currently struggling with getting some security to the Unity3d app I'm working on, and I want to add verification that apk file wasn't tampered with by some patchers. I know how to get keystore fingerprint from a built app (like keytool), but I have a hard time figuring out how to get that fingerprint from the code of my app during runtime to check that it is the same. I've gone through a lot of other threads to no success so far (like this: Get certificate fingerprint from android app)
Has anyone found solution how to do this or hint where I should start looking? Thanks in advance!
Modify the getCertificateSHA1Fingerprint function from the answer you linked to take Android Context as parameter. Get the Context from Unity and send it to this function then mark as static.
Java:
public final class CertificateSHA1Fingerprint
{
private static String getCertificateSHA1Fingerprint(Context mContext)
{
PackageManager pm = mContext.getPackageManager();
String packageName = mContext.getPackageName();
int flags = PackageManager.GET_SIGNATURES;
PackageInfo packageInfo = null;
try {
packageInfo = pm.getPackageInfo(packageName, flags);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
Signature[] signatures = packageInfo.signatures;
byte[] cert = signatures[0].toByteArray();
InputStream input = new ByteArrayInputStream(cert);
CertificateFactory cf = null;
try {
cf = CertificateFactory.getInstance("X509");
} catch (CertificateException e) {
e.printStackTrace();
}
X509Certificate c = null;
try {
c = (X509Certificate) cf.generateCertificate(input);
} catch (CertificateException e) {
e.printStackTrace();
}
String hexString = null;
try {
MessageDigest md = MessageDigest.getInstance("SHA1");
byte[] publicKey = md.digest(c.getEncoded());
hexString = byte2HexFormatted(publicKey);
} catch (NoSuchAlgorithmException e1) {
e1.printStackTrace();
} catch (CertificateEncodingException e) {
e.printStackTrace();
}
return hexString;
}
public static String byte2HexFormatted(byte[] arr)
{
StringBuilder str = new StringBuilder(arr.length * 2);
for (int i = 0; i < arr.length; i++)
{
String h = Integer.toHexString(arr[i]);
int l = h.length();
if (l == 1) h = "0" + h;
if (l > 2) h = h.substring(l - 2, l);
str.append(h.toUpperCase());
if (i < (arr.length - 1)) str.append(':');
}
return str.toString();
}
}
C#:
AndroidJavaClass unityClass;
AndroidJavaObject unityActivity;
AndroidJavaObject unityContext;
AndroidJavaClass customClass;
public string getCertificateSHA1Fingerprint()
{
//Replace with your full package name
string packageName = "com.example.CertificateSHA1Fingerprint";
unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
unityActivity = unityClass.GetStatic<AndroidJavaObject>("currentActivity");
unityContext = unityActivity.Call<AndroidJavaObject>("getApplicationContext");
customClass = new AndroidJavaClass(packageName);
string result = customClass.CallStatic<string>("getCertificateSHA1Fingerprint", unityContext);
return result;
}
You only have to build the Java function with Android Studio and convert it into Jar or .AAR file then put it in your <ProjectName>Assets\Plugins\Android folder. The C# code will be able to communicate with it.
I want to implement in-app purchase verification server-side in PHP language.
I tested this link but returned false for correct data set.
The java function:
public class Security {
private static final String TAG = "IABUtil/Security";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
TextUtils.isEmpty(signature)) {
Log.e(TAG, "Purchase verification failed: missing data.");
return false;
}
PublicKey key = Security.generatePublicKey(base64PublicKey);
return Security.verify(key, signedData, signature);
}
public static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
Log.e(TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
throw new IllegalArgumentException(e);
}
}
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
...
}
}
I tried this code:
function verify_market_in_app($signed_data, $signature, $public_key_base64)
{
$key = "-----BEGIN PUBLIC KEY-----\n".
chunk_split($public_key_base64, 64,"\n").
'-----END PUBLIC KEY-----';
//using PHP to create an RSA key
$key = openssl_get_publickey($key);
//$signature should be in binary format, but it comes as BASE64.
//So, I'll convert it.
$signature = base64_decode($signature);
//using PHP's native support to verify the signature
$result = openssl_verify(
$signed_data,
$signature,
$key,
OPENSSL_ALGO_SHA1);
if (0 === $result)
{
return false;
}
else if (1 !== $result)
{
return false;
}
else
{
return true;
}
}
but this isn't work correctly.
I use openssl_error_string() function and get this error:
error:0906D064:PEM routines:PEM_read_bio:bad base64 decode
Can any body help?
I'm trying to make a Facebook Chat on Android with the Smack library. I've read the Chat API from Facebook, but I cannot understand how I have to authenticate with Facebook using this library.
Can anyone point me how to accomplish this?
Update: According to the no.good.at.coding answer, I have this code adapted to the Asmack library. All works fine except I receive as response to the login: not-authorized. Here is the code I use:
public class SASLXFacebookPlatformMechanism extends SASLMechanism
{
private static final String NAME = "X-FACEBOOK-PLATFORM";
private String apiKey = "";
private String applicationSecret = "";
private String sessionKey = "";
/**
* Constructor.
*/
public SASLXFacebookPlatformMechanism(SASLAuthentication saslAuthentication)
{
super(saslAuthentication);
}
#Override
protected void authenticate() throws IOException, XMPPException
{
getSASLAuthentication().send(new AuthMechanism(NAME, ""));
}
#Override
public void authenticate(String apiKeyAndSessionKey, String host,
String applicationSecret) throws IOException, XMPPException
{
if (apiKeyAndSessionKey == null || applicationSecret == null)
{
throw new IllegalArgumentException("Invalid parameters");
}
String[] keyArray = apiKeyAndSessionKey.split("\\|", 2);
if (keyArray.length < 2)
{
throw new IllegalArgumentException(
"API key or session key is not present");
}
this.apiKey = keyArray[0];
Log.d("API_KEY", apiKey);
this.applicationSecret = applicationSecret;
Log.d("SECRET_KEY", applicationSecret);
this.sessionKey = keyArray[1];
Log.d("SESSION_KEY", sessionKey);
this.authenticationId = sessionKey;
this.password = applicationSecret;
this.hostname = host;
String[] mechanisms = { "DIGEST-MD5" };
Map<String, String> props = new HashMap<String, String>();
this.sc =
Sasl.createSaslClient(mechanisms, null, "xmpp", host, props,
this);
authenticate();
}
#Override
protected String getName()
{
return NAME;
}
#Override
public void challengeReceived(String challenge) throws IOException
{
byte[] response = null;
if (challenge != null)
{
String decodedChallenge = new String(Base64.decode(challenge));
Log.d("DECODED", decodedChallenge);
Map<String, String> parameters = getQueryMap(decodedChallenge);
String version = "1.0";
String nonce = parameters.get("nonce");
String method = parameters.get("method");
long callId = new GregorianCalendar().getTimeInMillis() / 1000L;
String sig =
"api_key=" + apiKey + "call_id=" + callId + "method="
+ method + "nonce=" + nonce + "session_key="
+ sessionKey + "v=" + version + applicationSecret;
try
{
sig = md5(sig);
sig = sig.toUpperCase();
} catch (NoSuchAlgorithmException e)
{
throw new IllegalStateException(e);
}
String composedResponse =
"api_key=" + URLEncoder.encode(apiKey, "utf-8")
+ "&call_id=" + callId + "&method="
+ URLEncoder.encode(method, "utf-8") + "&nonce="
+ URLEncoder.encode(nonce, "utf-8")
+ "&session_key="
+ URLEncoder.encode(sessionKey, "utf-8") + "&v="
+ URLEncoder.encode(version, "utf-8") + "&sig="
+ URLEncoder.encode(sig, "utf-8");
Log.d("COMPOSED", composedResponse);
response = composedResponse.getBytes("utf-8");
}
String authenticationText = "";
if (response != null)
{
authenticationText =
Base64.encodeBytes(response, Base64.DONT_BREAK_LINES);
}
// Send the authentication to the server
getSASLAuthentication().send(new Response(authenticationText));
}
private Map<String, String> getQueryMap(String query)
{
Map<String, String> map = new HashMap<String, String>();
String[] params = query.split("\\&");
for (String param : params)
{
String[] fields = param.split("=", 2);
map.put(fields[0], (fields.length > 1 ? fields[1] : null));
}
return map;
}
private String md5(String text) throws NoSuchAlgorithmException,
UnsupportedEncodingException
{
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(text.getBytes("utf-8"), 0, text.length());
return convertToHex(md.digest());
}
private String convertToHex(byte[] data)
{
StringBuilder buf = new StringBuilder();
int len = data.length;
for (int i = 0; i < len; i++)
{
int halfByte = (data[i] >>> 4) & 0xF;
int twoHalfs = 0;
do
{
if (0 <= halfByte && halfByte <= 9)
{
buf.append((char) ('0' + halfByte));
}
else
{
buf.append((char) ('a' + halfByte - 10));
}
halfByte = data[i] & 0xF;
} while (twoHalfs++ < 1);
}
return buf.toString();
}
}
And this, is the communication with the server with the sent and received messages:
PM SENT (1132418216): <stream:stream to="chat.facebook.com" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
PM RCV (1132418216): <?xml version="1.0"?><stream:stream id="C62D0F43" from="chat.facebook.com" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0" xml:lang="en"><stream:features><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>X-FACEBOOK-PLATFORM</mechanism><mechanism>DIGEST-MD5</mechanism></mechanisms></stream:features>
PM SENT (1132418216): <auth mechanism="X-FACEBOOK-PLATFORM" xmlns="urn:ietf:params:xml:ns:xmpp-sasl"></auth>
PM RCV (1132418216): <challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">dmVyc2lvbj0xJm1ldGhvZD1hdXRoLnhtcHBfbG9naW4mbm9uY2U9NzFGNkQ3Rjc5QkIyREJCQ0YxQTkwMzA0QTg3OTlBMzM=</challenge>
PM SENT (1132418216): <response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">YXBpX2tleT0zMWYzYjg1ZjBjODYwNjQ3NThiZTZhOTQyNjVjZmNjMCZjYWxsX2lkPTEzMDA0NTYxMzUmbWV0aG9kPWF1dGgueG1wcF9sb2dpbiZub25jZT03MUY2RDdGNzlCQjJEQkJDRjFBOTAzMDRBODc5OUEzMyZzZXNzaW9uX2tleT0yNjUzMTg4ODNkYWJhOGRlOTRiYTk4ZDYtMTAwMDAwNTAyNjc2Nzc4JnY9MS4wJnNpZz04RkRDRjRGRTgzMENGOEQ3QjgwNjdERUQyOEE2RERFQw==</response>
PM RCV (1132418216): <failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><not-authorized/></failure>
As read in the developers Facebook forum, it is needed to disable the "Disable Deprecated Auth Methods" setting from the Facebook settings page of your app. But, even doing that, I can't login. And the session key is the second part of the OAuth token in the form AAA|BBB|CCC, I mean, BBB.
Finally, thanks to the no.good.at.coding code and the suggestion of harism, I've been able to connect to the Facebook chat. This code is the Mechanism for the Asmack library (the Smack port for Android). For the Smack library is necessary to use the no.good.at.coding mechanism.
SASLXFacebookPlatformMechanism.java:
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
import org.apache.harmony.javax.security.sasl.Sasl;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.util.Base64;
public class SASLXFacebookPlatformMechanism extends SASLMechanism
{
private static final String NAME = "X-FACEBOOK-PLATFORM";
private String apiKey = "";
private String applicationSecret = "";
private String sessionKey = "";
/**
* Constructor.
*/
public SASLXFacebookPlatformMechanism(SASLAuthentication saslAuthentication)
{
super(saslAuthentication);
}
#Override
protected void authenticate() throws IOException, XMPPException
{
getSASLAuthentication().send(new AuthMechanism(NAME, ""));
}
#Override
public void authenticate(String apiKeyAndSessionKey, String host,
String applicationSecret) throws IOException, XMPPException
{
if (apiKeyAndSessionKey == null || applicationSecret == null)
{
throw new IllegalArgumentException("Invalid parameters");
}
String[] keyArray = apiKeyAndSessionKey.split("\\|", 2);
if (keyArray.length < 2)
{
throw new IllegalArgumentException(
"API key or session key is not present");
}
this.apiKey = keyArray[0];
this.applicationSecret = applicationSecret;
this.sessionKey = keyArray[1];
this.authenticationId = sessionKey;
this.password = applicationSecret;
this.hostname = host;
String[] mechanisms = { "DIGEST-MD5" };
Map<String, String> props = new HashMap<String, String>();
this.sc =
Sasl.createSaslClient(mechanisms, null, "xmpp", host, props,
this);
authenticate();
}
#Override
public void authenticate(String username, String host, CallbackHandler cbh)
throws IOException, XMPPException
{
String[] mechanisms = { "DIGEST-MD5" };
Map<String, String> props = new HashMap<String, String>();
this.sc =
Sasl.createSaslClient(mechanisms, null, "xmpp", host, props,
cbh);
authenticate();
}
#Override
protected String getName()
{
return NAME;
}
#Override
public void challengeReceived(String challenge) throws IOException
{
byte[] response = null;
if (challenge != null)
{
String decodedChallenge = new String(Base64.decode(challenge));
Map<String, String> parameters = getQueryMap(decodedChallenge);
String version = "1.0";
String nonce = parameters.get("nonce");
String method = parameters.get("method");
long callId = new GregorianCalendar().getTimeInMillis();
String sig =
"api_key=" + apiKey + "call_id=" + callId + "method="
+ method + "nonce=" + nonce + "session_key="
+ sessionKey + "v=" + version + applicationSecret;
try
{
sig = md5(sig);
} catch (NoSuchAlgorithmException e)
{
throw new IllegalStateException(e);
}
String composedResponse =
"api_key=" + URLEncoder.encode(apiKey, "utf-8")
+ "&call_id=" + callId + "&method="
+ URLEncoder.encode(method, "utf-8") + "&nonce="
+ URLEncoder.encode(nonce, "utf-8")
+ "&session_key="
+ URLEncoder.encode(sessionKey, "utf-8") + "&v="
+ URLEncoder.encode(version, "utf-8") + "&sig="
+ URLEncoder.encode(sig, "utf-8");
response = composedResponse.getBytes("utf-8");
}
String authenticationText = "";
if (response != null)
{
authenticationText =
Base64.encodeBytes(response, Base64.DONT_BREAK_LINES);
}
// Send the authentication to the server
getSASLAuthentication().send(new Response(authenticationText));
}
private Map<String, String> getQueryMap(String query)
{
Map<String, String> map = new HashMap<String, String>();
String[] params = query.split("\\&");
for (String param : params)
{
String[] fields = param.split("=", 2);
map.put(fields[0], (fields.length > 1 ? fields[1] : null));
}
return map;
}
private String md5(String text) throws NoSuchAlgorithmException,
UnsupportedEncodingException
{
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(text.getBytes("utf-8"), 0, text.length());
return convertToHex(md.digest());
}
private String convertToHex(byte[] data)
{
StringBuilder buf = new StringBuilder();
int len = data.length;
for (int i = 0; i < len; i++)
{
int halfByte = (data[i] >>> 4) & 0xF;
int twoHalfs = 0;
do
{
if (0 <= halfByte && halfByte <= 9)
{
buf.append((char) ('0' + halfByte));
}
else
{
buf.append((char) ('a' + halfByte - 10));
}
halfByte = data[i] & 0xF;
} while (twoHalfs++ < 1);
}
return buf.toString();
}
}
To use it:
ConnectionConfiguration config = new ConnectionConfiguration("chat.facebook.com", 5222);
config.setSASLAuthenticationEnabled(true);
XMPPConnection xmpp = new XMPPConnection(config);
try
{
SASLAuthentication.registerSASLMechanism("X-FACEBOOK-PLATFORM", SASLXFacebookPlatformMechanism.class);
SASLAuthentication.supportSASLMechanism("X-FACEBOOK-PLATFORM", 0);
xmpp.connect();
xmpp.login(apiKey + "|" + sessionKey, sessionSecret, "Application");
} catch (XMPPException e)
{
xmpp.disconnect();
e.printStackTrace();
}
apiKey is the API key given in the application settings page in Facebook. sessionKey is the second part of the access token. If the token is in this form, AAA|BBB|CCC, the BBB is the session key. sessionSecret is obtained using the old REST API with the method auth.promoteSession. To use it, it's needed to make a Http get to this url:
https://api.facebook.com/method/auth.promoteSession?access_token=yourAccessToken
Despite of the Facebook Chat documentation says that it's needed to use your application secret key, only when I used the key that returned that REST method I was able to make it works. To make that method works, you have to disable the Disable Deprecated Auth Methods option in the Advance tab in your application settings.
I'd used this about 6 months ago with Smack (not asmack) so I'm not sure how it'll hold up now but here goes, hope it helps!
I found an implementation of Facebook's X-FACEBOOK-PLATFORM authentication mechanism on the Ignite Realtime Smack forum where someone got it from the fbgc project. You'll find the a ZIP with the SASLXFacebookPlatformMechanism.java source in the answer I linked to. You can use it as follows:
public void login() throws XMPPException
{
SASLAuthentication.registerSASLMechanism(SASLXFacebookPlatformMechanism.NAME,
SASLXFacebookPlatformMechanism.class);
SASLAuthentication.supportSASLMechanism(SASLXFacebookPlatformMechanism.NAME, 0);
ConnectionConfiguration connConfig = new ConnectionConfiguration(host, port);
XMPPConnection connection = new XMPPConnection(connConfig);
connection.connect();
log.info("XMPP client connected");
connection.login(Utils.FB_APP_ID + "|" + this.user.sessionId, Utils.FB_APP_SECRET, "app_name");
log.info("XMPP client logged in");
}
I was doing this on the server without an SDK. I don't remember the details (and the Facebook documentation isn't very good) but from what I can tell from my code, after getting the user to authorize the app, I get a callback request from Facebook with a code parameter. I open a URLConnection to https://graph.facebook.com/oauth/access_token?client_id=<app_id>&redirect_uri=http://myserver/context/path/&client_secret=<app_secret>&code=<code>. The response should be the access token where the session id is the part after the | - something of the form XXX|<sessionId>.
Here's code I've been using successfully for authentication. Maybe this helps even though this is not related to Smack in any way. You can get sessionKey from access token received from FB, and for getting sessionSecret I've been using oldish REST API;
http://developers.facebook.com/docs/reference/rest/auth.promoteSession/
private final void processChallenge(XmlPullParser parser, Writer writer,
String sessionKey, String sessionSecret) throws IOException,
NoSuchAlgorithmException, XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, "challenge");
String challenge = new String(Base64.decode(parser.nextText(),
Base64.DEFAULT));
String params[] = challenge.split("&");
HashMap<String, String> paramMap = new HashMap<String, String>();
for (int i = 0; i < params.length; ++i) {
String p[] = params[i].split("=");
p[0] = URLDecoder.decode(p[0]);
p[1] = URLDecoder.decode(p[1]);
paramMap.put(p[0], p[1]);
}
String api_key = "YOUR_API_KEY";
String call_id = "" + System.currentTimeMillis();
String method = paramMap.get("method");
String nonce = paramMap.get("nonce");
String v = "1.0";
StringBuffer sigBuffer = new StringBuffer();
sigBuffer.append("api_key=" + api_key);
sigBuffer.append("call_id=" + call_id);
sigBuffer.append("method=" + method);
sigBuffer.append("nonce=" + nonce);
sigBuffer.append("session_key=" + sessionKey);
sigBuffer.append("v=" + v);
sigBuffer.append(sessionSecret);
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(sigBuffer.toString().getBytes());
byte[] digest = md.digest();
StringBuffer sig = new StringBuffer();
for (int i = 0; i < digest.length; ++i) {
sig.append(Integer.toHexString(0xFF & digest[i]));
}
StringBuffer response = new StringBuffer();
response.append("api_key=" + URLEncoder.encode(api_key));
response.append("&call_id=" + URLEncoder.encode(call_id));
response.append("&method=" + URLEncoder.encode(method));
response.append("&nonce=" + URLEncoder.encode(nonce));
response.append("&session_key=" + URLEncoder.encode(sessionKey));
response.append("&v=" + URLEncoder.encode(v));
response.append("&sig=" + URLEncoder.encode(sig.toString()));
StringBuilder out = new StringBuilder();
out.append("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>");
out.append(Base64.encodeToString(response.toString().getBytes(),
Base64.NO_WRAP));
out.append("</response>");
writer.write(out.toString());
writer.flush();
}
I'm sorry to make new answer but I had to include the new code #YShinkarev sorry for being late
By modifying #Adrian answer to make challengeReceived we can use APIKey and accessToken all I modified was the composedResponse
#Override
public void challengeReceived(String challenge) throws IOException {
byte[] response = null;
if (challenge != null) {
String decodedChallenge = new String(Base64.decode(challenge));
Map<String, String> parameters = getQueryMap(decodedChallenge);
String version = "1.0";
String nonce = parameters.get("nonce");
String method = parameters.get("method");
long callId = new GregorianCalendar().getTimeInMillis();
String composedResponse = "api_key="
+ URLEncoder.encode(apiKey, "utf-8") + "&call_id=" + callId
+ "&method=" + URLEncoder.encode(method, "utf-8")
+ "&nonce=" + URLEncoder.encode(nonce, "utf-8")
+ "&access_token="
+ URLEncoder.encode(access_token, "utf-8") + "&v="
+ URLEncoder.encode(version, "utf-8");
response = composedResponse.getBytes("utf-8");
}
String authenticationText = "";
if (response != null) {
authenticationText = Base64.encodeBytes(response,
Base64.DONT_BREAK_LINES);
}
// Send the authentication to the server
getSASLAuthentication().send(new Response(authenticationText));
}
What do you want to do?
If you just want to login on FB chat, you connect to FB just like any other XMPP server.
I would look at and use "Authenticating with Username/Password" from Chat API, wich is supported by Smack. Unless I would like to write an FaceBook-application. Then I would try to login in with "Authenticating with Facebook Platform".
So, just use Smack to connect to FB chat as you would do with your ordinary Jabber client.
For the username, use your Facebook username. (see http://www.facebook.com/username/ )
For the domain, use: chat.facebook.com
For the password, use your Facebook password
Turn off SSL and TSL
Set connect port to: 5222 (which is the default for XMPP)
Set connect server to chat.facebook.com