I have implemented inapp purchasing in my application.
When I buy a managed application it shows me this error log.
Can anyone tell me why this happen?
Here is my logcat:
08-31 14:58:00.931: E/IABUtil/Security(31515): Purchase verification failed: missing data.
08-31 14:58:00.931: E/viable(31515): Public key signature doesn't match!
and here is my Security.java Class
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package Classes;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
/**
* Security-related methods. For a secure implementation, all of this code
* should be implemented on a server that communicates with the
* application on the device. For the sake of simplicity and clarity of this
* example, this code is included here and is executed on the device. If you
* must verify the purchases on the phone, you should obfuscate this code to
* make it harder for an attacker to replace the code with stubs that treat all
* purchases as verified.
*/
class Security {
private static final String TAG = "IABUtil/Security";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
* Verifies that the data was signed with the given signature, and returns
* the verified purchase. The data is in JSON format and signed
* with a private key. The data also contains the {#link PurchaseState}
* and product ID of the purchase.
* #param base64PublicKey the base64-encoded public key to use for verifying.
* #param signedData the signed JSON string (signed, not encrypted)
* #param signature the signature for the data, signed with the private key
*/
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);
}
/**
* Generates a PublicKey instance from a string containing the
* Base64-encoded public key.
*
* #param encodedPublicKey Base64-encoded public key
* #throws IllegalArgumentException if encodedPublicKey is invalid
*/
public static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
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 (IllegalArgumentException e) {
Log.e(TAG, "Base64 decoding failed.");
throw e;
}
}
/**
* Verifies that the signature from the server matches the computed
* signature on the data. Returns true if the data is correctly signed.
*
* #param publicKey public key associated with the developer account
* #param signedData signed data from server
* #param signature server signature
* #return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
Signature sig;
try {
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature, Base64.DEFAULT))) {
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
} catch (IllegalArgumentException e) {
Log.e(TAG, "Base64 decoding failed.");
}
return false;
}
}
I think there is a problem regarding your App public key / License Key for the application.
http://developer.android.com/google/play/billing/api.html
Related
I'm able to successfully add my images to google drive, using the API provided by Google Drive.
Can I now retrieve it back to my App?
If yes, Can anyone help with how to do that?
Use this to retrieve files from the Google Drive API
https://www.googleapis.com/drive/v3/files/[FILEID]?key=[YOUR_API_KEY]'
If you want the entirety of code required to retrieve a file, here it is:
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpResponse;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import java.io.IOException;
import java.io.InputStream;
// ...
public class MyClass {
// ...
/**
* Print a file's metadata.
*
* #param service Drive API service instance.
* #param fileId ID of the file to print metadata for.
*/
private static void printFile(Drive service, String fileId) {
try {
File file = service.files().get(fileId).execute();
System.out.println("Title: " + file.getTitle());
System.out.println("Description: " + file.getDescription());
System.out.println("MIME type: " + file.getMimeType());
} catch (IOException e) {
System.out.println("An error occurred: " + e);
}
}
/**
* Download a file's content.
*
* #param service Drive API service instance.
* #param file Drive File instance.
* #return InputStream containing the file's content if successful,
* {#code null} otherwise.
*/
private static InputStream downloadFile(Drive service, File file) {
if (file.getDownloadUrl() != null && file.getDownloadUrl().length() > 0) {
try {
HttpResponse resp =
service.getRequestFactory().buildGetRequest(new GenericUrl(file.getDownloadUrl()))
.execute();
return resp.getContent();
} catch (IOException e) {
// An error occurred.
e.printStackTrace();
return null;
}
} else {
// The file doesn't have any content stored on Drive.
return null;
}
}
// ...
}
When using in-app billing, you should verify the purchase data, by checking if the INAPP_PURCHASE_DATA was signed with the INAPP_DATA_SIGNATURE by using the base64 encoded public key from Google Play store.
See the explanation of INAPP_PURCHASE_DATA and INAPP_DATA_SIGNATURE here.
There is a Security class you can use to verify the purchase:
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";
/**
* Verifies that the data was signed with the given signature, and returns
* the verified purchase. The data is in JSON format and signed
* with a private key. The data also contains the {#link PurchaseState}
* and product ID of the purchase.
* #param base64PublicKey the base64-encoded public key to use for verifying.
* #param signedData the signed JSON string (signed, not encrypted)
* #param signature the signature for the data, signed with the private key
*/
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);
}
/**
* Generates a PublicKey instance from a string containing the
* Base64-encoded public key.
*
* #param encodedPublicKey Base64-encoded public key
* #throws IllegalArgumentException if encodedPublicKey is invalid
*/
public static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
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);
}
}
/**
* Verifies that the signature from the server matches the computed
* signature on the data. Returns true if the data is correctly signed.
*
* #param publicKey public key associated with the developer account
* #param signedData signed data from server
* #param signature server signature
* #return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
byte[] signatureBytes;
try {
signatureBytes = Base64.decode(signature, Base64.DEFAULT);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Base64 decoding failed.");
return false;
}
try {
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(signatureBytes)) {
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
}
return false;
}
}
You have to invoke the verifyPurchase and pass the purchase data, the given signature and the base64PublicKey public key from Google Play. There is a wrapper implementation I can use for the purchase flow in my app.
If you look into the IabHelper implementation, they pass the public key for the verification in constructor. The documentation of the ctor says:
* #param base64PublicKey Your application's public key, encoded in base64.
* This is used for verification of purchase signatures. You can find your app's base64-encoded
* public key in your application's page on Google Play Developer Console. Note that this
* is NOT your "developer public key".
*/
I guess they mean the Base64-encoded RSA public key in Licensing & in-app billing section in Google Play:
Maybe I don't know enough about cryptography but how is this possible that I use a public key from Google Play to check an encryption which is supposedly made with my "developers private key" (see explanation in first link). Do they mean my "private key I used to sign the app"? I don't think so, because they cannot know my (local) private key (I use to sign my app) and what does it have to do with this public key from Google Play, so what do they mean with "developer's private key".
So my questions are:
Did I understand it right that the public key is the key from
Licensing & in-app billing?
Do I also need to add licensing to my app to get this
verification working or should this work "out of the box", so can I
omit this step?
What is the "developer's private key" Google is using to sign the
purchase data and where do I see it? (I need to run some Unit tests
on my server to check my implementation and I want to encrypt the
INAPP_PURCHASE_DATA to get the INAPP_DATA_SIGNATURE as well to be
able to get a valid security check if I verify it with the given
public key.
[UPDATE]. Obviously, the private key is hidden:
The Google Play Console exposes the public key for licensing to any developer signed in to the Play Console, but it keeps the private key hidden from all users in a secure location.
See: https://developer.android.com/google/play/licensing/adding-licensing.html
There are two types of signature asymmetric & symmetric :
Asymm uses a pair of keys, the private and the public, the keys have a mathematical relationship between them, one chunk of data signed with the private key can be verified with the public one. The private key is never published, but the public is.
Then Google created a pair of keys for your in-app billing ... but you only need to know the public to verify.
No body will generate a valid signature without the private key.
Instead Symm uses the same key in both sides, that poses the problem to share the key with the risk to be sniffed, but it has the advantage to be faster than asymm.
UPDATE
Do I also need to add licensing to my app to get this verification
working or should this work "out of the box", so can I omit this step?
Depends, if you want to know if the app has been installed from the official Google Play Store then you need verify licensing, that applies better if your app is a paid app, instead if your app is free but it has in-app products the important thing is to know if they purchased legally the item.
For me it is more important to verify purchases in a external server, there you have a nice example https://stackoverflow.com/a/48645216/7690376
I'm trying to search youtube video based on given keyword. So that I can list all found video to display in a list with video thumbnail. I don't find a way to do so. After searching I found this https://developers.google.com/youtube/v3/docs/videos/list#examples.
Can I use save for my android application? Or how do I search in Android application?
You can use the Youtube API code samples in your app on the following conditions:
1. Your app is a free app.
2. If your app is monetized (paid, ads, and/or in app purchases) then your app has to contain more content and features than just a feature searching Youtube.
Full details here:
https://developers.google.com/youtube/creating_monetizable_applications
That being said, you can fully use code samples from Youtube API. I have done so myself to create an Android app to help people find all my favorite cat videos (shameless promotion): Cat Roulette
Anyhow, here is a specific Java code example for searching Youtube videos:
https://developers.google.com/youtube/v3/code_samples/java#search_by_keyword
/*
* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.api.services.samples.youtube.cmdline.data;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.samples.youtube.cmdline.Auth;
import com.google.api.services.youtube.YouTube;
import com.google.api.services.youtube.model.ResourceId;
import com.google.api.services.youtube.model.SearchListResponse;
import com.google.api.services.youtube.model.SearchResult;
import com.google.api.services.youtube.model.Thumbnail;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
/**
* Print a list of videos matching a search term.
*
* #author Jeremy Walker
*/
public class Search {
/**
* Define a global variable that identifies the name of a file that
* contains the developer's API key.
*/
private static final String PROPERTIES_FILENAME = "youtube.properties";
private static final long NUMBER_OF_VIDEOS_RETURNED = 25;
/**
* Define a global instance of a Youtube object, which will be used
* to make YouTube Data API requests.
*/
private static YouTube youtube;
/**
* Initialize a YouTube object to search for videos on YouTube. Then
* display the name and thumbnail image of each video in the result set.
*
* #param args command line args.
*/
public static void main(String[] args) {
// Read the developer key from the properties file.
Properties properties = new Properties();
try {
InputStream in = Search.class.getResourceAsStream("/" + PROPERTIES_FILENAME);
properties.load(in);
} catch (IOException e) {
System.err.println("There was an error reading " + PROPERTIES_FILENAME + ": " + e.getCause()
+ " : " + e.getMessage());
System.exit(1);
}
try {
// This object is used to make YouTube Data API requests. The last
// argument is required, but since we don't need anything
// initialized when the HttpRequest is initialized, we override
// the interface and provide a no-op function.
youtube = new YouTube.Builder(Auth.HTTP_TRANSPORT, Auth.JSON_FACTORY, new HttpRequestInitializer() {
public void initialize(HttpRequest request) throws IOException {
}
}).setApplicationName("youtube-cmdline-search-sample").build();
// Prompt the user to enter a query term.
String queryTerm = getInputQuery();
// Define the API request for retrieving search results.
YouTube.Search.List search = youtube.search().list("id,snippet");
// Set your developer key from the Google Developers Console for
// non-authenticated requests. See:
// https://console.developers.google.com/
String apiKey = properties.getProperty("youtube.apikey");
search.setKey(apiKey);
search.setQ(queryTerm);
// Restrict the search results to only include videos. See:
// https://developers.google.com/youtube/v3/docs/search/list#type
search.setType("video");
// To increase efficiency, only retrieve the fields that the
// application uses.
search.setFields("items(id/kind,id/videoId,snippet/title,snippet/thumbnails/default/url)");
search.setMaxResults(NUMBER_OF_VIDEOS_RETURNED);
// Call the API and print results.
SearchListResponse searchResponse = search.execute();
List<SearchResult> searchResultList = searchResponse.getItems();
if (searchResultList != null) {
prettyPrint(searchResultList.iterator(), queryTerm);
}
} catch (GoogleJsonResponseException e) {
System.err.println("There was a service error: " + e.getDetails().getCode() + " : "
+ e.getDetails().getMessage());
} catch (IOException e) {
System.err.println("There was an IO error: " + e.getCause() + " : " + e.getMessage());
} catch (Throwable t) {
t.printStackTrace();
}
}
/*
* Prompt the user to enter a query term and return the user-specified term.
*/
private static String getInputQuery() throws IOException {
String inputQuery = "";
System.out.print("Please enter a search term: ");
BufferedReader bReader = new BufferedReader(new InputStreamReader(System.in));
inputQuery = bReader.readLine();
if (inputQuery.length() < 1) {
// Use the string "YouTube Developers Live" as a default.
inputQuery = "YouTube Developers Live";
}
return inputQuery;
}
/*
* Prints out all results in the Iterator. For each result, print the
* title, video ID, and thumbnail.
*
* #param iteratorSearchResults Iterator of SearchResults to print
*
* #param query Search query (String)
*/
private static void prettyPrint(Iterator<SearchResult> iteratorSearchResults, String query) {
System.out.println("\n=============================================================");
System.out.println(
" First " + NUMBER_OF_VIDEOS_RETURNED + " videos for search on \"" + query + "\".");
System.out.println("=============================================================\n");
if (!iteratorSearchResults.hasNext()) {
System.out.println(" There aren't any results for your query.");
}
while (iteratorSearchResults.hasNext()) {
SearchResult singleVideo = iteratorSearchResults.next();
ResourceId rId = singleVideo.getId();
// Confirm that the result represents a video. Otherwise, the
// item will not contain a video ID.
if (rId.getKind().equals("youtube#video")) {
Thumbnail thumbnail = singleVideo.getSnippet().getThumbnails().getDefault();
System.out.println(" Video Id" + rId.getVideoId());
System.out.println(" Title: " + singleVideo.getSnippet().getTitle());
System.out.println(" Thumbnail: " + thumbnail.getUrl());
System.out.println("\n-------------------------------------------------------------\n");
}
}
}
}
FYI you have to also obtain a Youtube API key as explained here:
https://developers.google.com/youtube/registering_an_application?hl=en
I need to create an SSL self signed cert on the fly in an Android app and be able to use it from an https server in the same app. I found this code to create a cert although I'm not sure it is the right kind of cert. And I haven't found much regarding how to add that to the BouncyCastle keystore on my app and then how to use it when creating the HTTPs server. Can someone point me to an example which does this? Thank you.
static X509Certificate generateSelfSignedX509Certificate() throws Exception {
// yesterday
Date validityBeginDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
// in 2 years
Date validityEndDate = new Date(System.currentTimeMillis() + 2 * 365 * 24 * 60 * 60 * 1000);
// GENERATE THE PUBLIC/PRIVATE RSA KEY PAIR
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(1024, new SecureRandom());
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// GENERATE THE X509 CERTIFICATE
X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
X500Principal dnName = new X500Principal("CN=John Doe");
certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
certGen.setSubjectDN(dnName);
certGen.setIssuerDN(dnName); // use the same
certGen.setNotBefore(validityBeginDate);
certGen.setNotAfter(validityEndDate);
certGen.setPublicKey(keyPair.getPublic());
certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
X509Certificate cert = certGen.generate(keyPair.getPrivate(), "BC");
// DUMP CERTIFICATE AND KEY PAIR
return cert;
// System.out.println(cert);
}
The following solution works for generating a self signed certificate using Spongy Castle (Bouncy Castle) on Android. I've tested the code with Android 10 (Q) and Android Pie.
This code is a modified version of Netty's io.netty.handler.ssl.util.SelfSignedCertificate. The original version requires Bouncy Castle; which does not seem to be present by default on Android 10 resulting in a java.lang.NoClassDefFoundError: org.spongycastle.jce.provider.BouncyCastleProvider. Hence, I had to copy over the code and modify it to get it working with Spongy Castle.
build.gradle
dependencies {
implementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0'
}
SelfSignedCertificate.java
import android.util.Base64;
import android.util.Log;
import org.spongycastle.asn1.x500.X500Name;
import org.spongycastle.cert.X509CertificateHolder;
import org.spongycastle.cert.X509v3CertificateBuilder;
import org.spongycastle.cert.jcajce.JcaX509CertificateConverter;
import org.spongycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.spongycastle.operator.ContentSigner;
import org.spongycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;
public final class SelfSignedCertificate {
private static final String TAG = SelfSignedCertificate.class.getSimpleName();
/**
* Current time minus 1 year, just in case software clock goes back due to time synchronization
*/
private static final Date DEFAULT_NOT_BEFORE = new Date(System.currentTimeMillis() - 86400000L * 365);
/**
* The maximum possible value in X.509 specification: 9999-12-31 23:59:59
*/
private static final Date DEFAULT_NOT_AFTER = new Date(253402300799000L);
/**
* FIPS 140-2 encryption requires the key length to be 2048 bits or greater.
* Let's use that as a sane default but allow the default to be set dynamically
* for those that need more stringent security requirements.
*/
private static final int DEFAULT_KEY_LENGTH_BITS = 2048;
/**
* FQDN to use if none is specified.
*/
private static final String DEFAULT_FQDN = "example.com";
/**
* 7-bit ASCII, as known as ISO646-US or the Basic Latin block of the
* Unicode character set
*/
private static final Charset US_ASCII = Charset.forName("US-ASCII");
private static final Provider provider = new BouncyCastleProvider();
private final File certificate;
private final File privateKey;
private final X509Certificate cert;
private final PrivateKey key;
/**
* Creates a new instance.
*/
public SelfSignedCertificate() throws CertificateException {
this(DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
}
/**
* Creates a new instance.
*
* #param notBefore Certificate is not valid before this time
* #param notAfter Certificate is not valid after this time
*/
public SelfSignedCertificate(Date notBefore, Date notAfter) throws CertificateException {
this("example.com", notBefore, notAfter);
}
/**
* Creates a new instance.
*
* #param fqdn a fully qualified domain name
*/
public SelfSignedCertificate(String fqdn) throws CertificateException {
this(fqdn, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
}
/**
* Creates a new instance.
*
* #param fqdn a fully qualified domain name
* #param notBefore Certificate is not valid before this time
* #param notAfter Certificate is not valid after this time
*/
public SelfSignedCertificate(String fqdn, Date notBefore, Date notAfter) throws CertificateException {
// Bypass entropy collection by using insecure random generator.
// We just want to generate it without any delay because it's for testing purposes only.
this(fqdn, new SecureRandom(), DEFAULT_KEY_LENGTH_BITS, notBefore, notAfter);
}
/**
* Creates a new instance.
*
* #param fqdn a fully qualified domain name
* #param random the {#link java.security.SecureRandom} to use
* #param bits the number of bits of the generated private key
*/
public SelfSignedCertificate(String fqdn, SecureRandom random, int bits) throws CertificateException {
this(fqdn, random, bits, DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER);
}
/**
* Creates a new instance.
*
* #param fqdn a fully qualified domain name
* #param random the {#link java.security.SecureRandom} to use
* #param bits the number of bits of the generated private key
* #param notBefore Certificate is not valid before this time
* #param notAfter Certificate is not valid after this time
*/
public SelfSignedCertificate(String fqdn, SecureRandom random, int bits, Date notBefore, Date notAfter)
throws CertificateException {
// Generate an RSA key pair.
final KeyPair keypair;
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(bits, random);
keypair = keyGen.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
// Should not reach here because every Java implementation must have RSA key pair generator.
throw new Error(e);
}
String[] paths;
try {
// Try Bouncy Castle if the current JVM didn't have sun.security.x509.
paths = generateCertificate(fqdn, keypair, random, notBefore, notAfter);
} catch (Throwable t2) {
Log.d(TAG, "Failed to generate a self-signed X.509 certificate using Bouncy Castle:", t2);
throw new CertificateException("No provider succeeded to generate a self-signed certificate. See debug log for the root cause.", t2);
}
certificate = new File(paths[0]);
privateKey = new File(paths[1]);
key = keypair.getPrivate();
FileInputStream certificateInput = null;
try {
certificateInput = new FileInputStream(certificate);
cert = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(certificateInput);
} catch (Exception e) {
throw new CertificateEncodingException(e);
} finally {
if (certificateInput != null) {
try {
certificateInput.close();
} catch (IOException e) {
Log.w(TAG, "Failed to close a file: " + certificate, e);
}
}
}
}
/**
* Returns the generated X.509 certificate file in PEM format.
*/
public File certificate() {
return certificate;
}
/**
* Returns the generated RSA private key file in PEM format.
*/
public File privateKey() {
return privateKey;
}
/**
* Returns the generated X.509 certificate.
*/
public X509Certificate cert() {
return cert;
}
/**
* Returns the generated RSA private key.
*/
public PrivateKey key() {
return key;
}
/**
* Deletes the generated X.509 certificate file and RSA private key file.
*/
public void delete() {
safeDelete(certificate);
safeDelete(privateKey);
}
private static String[] generateCertificate(String fqdn, KeyPair keypair, SecureRandom random, Date notBefore, Date notAfter)
throws Exception {
PrivateKey key = keypair.getPrivate();
// Prepare the information required for generating an X.509 certificate.
X500Name owner = new X500Name("CN=" + fqdn);
X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
owner, new BigInteger(64, random), notBefore, notAfter, owner, keypair.getPublic());
ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(key);
X509CertificateHolder certHolder = builder.build(signer);
X509Certificate cert = new JcaX509CertificateConverter().setProvider(provider).getCertificate(certHolder);
cert.verify(keypair.getPublic());
return newSelfSignedCertificate(fqdn, key, cert);
}
private static String[] newSelfSignedCertificate(String fqdn, PrivateKey key, X509Certificate cert) throws IOException, CertificateEncodingException {
String keyText = "-----BEGIN PRIVATE KEY-----\n" + Base64.encodeToString(key.getEncoded(), Base64.DEFAULT) + "\n-----END PRIVATE KEY-----\n";
File keyFile = File.createTempFile("keyutil_" + fqdn + '_', ".key");
keyFile.deleteOnExit();
OutputStream keyOut = new FileOutputStream(keyFile);
try {
keyOut.write(keyText.getBytes(US_ASCII));
keyOut.close();
keyOut = null;
} finally {
if (keyOut != null) {
safeClose(keyFile, keyOut);
safeDelete(keyFile);
}
}
String certText = "-----BEGIN CERTIFICATE-----\n" + Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT) + "\n-----END CERTIFICATE-----\n";
File certFile = File.createTempFile("keyutil_" + fqdn + '_', ".crt");
certFile.deleteOnExit();
OutputStream certOut = new FileOutputStream(certFile);
try {
certOut.write(certText.getBytes(US_ASCII));
certOut.close();
certOut = null;
} finally {
if (certOut != null) {
safeClose(certFile, certOut);
safeDelete(certFile);
safeDelete(keyFile);
}
}
return new String[]{certFile.getPath(), keyFile.getPath()};
}
private static void safeDelete(File certFile) {
if (!certFile.delete()) {
Log.w(TAG, "Failed to delete a file: " + certFile);
}
}
private static void safeClose(File keyFile, OutputStream keyOut) {
try {
keyOut.close();
} catch (IOException e) {
Log.w(TAG, "Failed to close a file: " + keyFile, e);
}
}
}
Usage
private SslContext getSslContext() throws CertificateException, SSLException {
SelfSignedCertificate ssc = new SelfSignedCertificate(BuildConfig.APPLICATION_ID);
return SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).protocols("TLSv1.2").build();
}
I pass this SslContext to create the ChannelPipeline for starting a Netty server with HTTPS support, but you may use the generated certificate any way you like.
To use the certificate from the answer above in Android (100% working code):
try {
final SelfSignedCertificate ssc = new SelfSignedCertificate(BuildConfig.APPLICATION_ID);
final KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(null, null);
keyStore.setKeyEntry("key", ssc.key(), null, new X509Certificate[]{ssc.cert()});
final KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509");
kmf.init(keyStore, null);
final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
final SSLContext context = SSLContext.getInstance("TLSv1.2");
context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
webServer = (SSLServerSocket) context.getServerSocketFactory().createServerSocket(webserverPort);
} catch (Exception e) {
e.printStackTrace();
}
And a minimized version to the method (who will write this in one line? )))):
private Pair<X509Certificate, PrivateKey> selfSignedCertificate(String fqdn) {
final SecureRandom random = new SecureRandom();
final KeyPairGenerator keyGen;
try {
keyGen = KeyPairGenerator.getInstance("RSA", "BC");
} catch (Exception e) {
return null;
}
keyGen.initialize(2048, random);
final KeyPair keypair = keyGen.generateKeyPair();
final PrivateKey key = keypair.getPrivate();
final X509Certificate cert;
try {
final X500Name owner = new X500Name("CN=" + fqdn);
final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(owner, new BigInteger(64, random), new Date(System.currentTimeMillis() - 86400000L * 365), new Date(253402300799000L), owner, keypair.getPublic());
final ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(key);
final X509CertificateHolder certHolder = builder.build(signer);
cert = new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider()).getCertificate(certHolder);
cert.verify(keypair.getPublic());
} catch (Throwable t) {
return null;
}
return new Pair(cert, key);
}
Background:
We have an Android app that is currently on sale via Google Play. For the app to function the user must purchase a "token" via In-App Billing. The "token" is a consumable item, eg used once and finished with. To verify the token, we send the purchase data to a server which uses standard Java RSA security code to verify the information returned from the Play Store is valid. (Code below).
We did extensive testing prior to releasing the app, and even once the app is on the store, we did some more testing. The data being returned from Google passed verification every time. Then about the start of December the signature verification started failing. We haven't changed the code or the app in the store, and the verification code on the server has remained static.
I've debugged the code, and ran the receipt data and signature data being returned from the Play Store and it indeed now fails verification. I'm at a loss to explain what has changed, or why the verification started failing, when it was working fine.
Question:
Has anyone come across this before, where signature verification failed in an app that hasn't changed? Any tips on where to start looking to try and work out where the issues may be coming from?
Further Information
The only thing that I can think of changing, is Google released the in-app billing API v3, but that shouldn't effect V2, which is what we use.
To aide development, we use the net.robotmedia.billing library to handle the IAB.
Below is the server verification code for data returned from Play Store
where encodePublicKey => our public key from Play Store
signedData => base64 encoded receiptData as return from Play Store purchase
signature => signature as returned from Play Store
public class Security {
public final static Logger logger = Logger.getLogger(Security.class.getName());
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
* Generates a PublicKey instance from a string containing the
* Base64-encoded public key.
*
* #param encodedPublicKey
* Base64-encoded public key
* #throws IllegalArgumentException
* if encodedPublicKey is invalid
*/
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) {
logger.error("Invalid key specification.", e);
throw new IllegalArgumentException(e);
}
catch (Base64DecoderException e) {
logger.error("Base64 decoding failed.", e);
throw new IllegalArgumentException(e);
}
}
/**
* Verifies that the signature from the server matches the computed
* signature on the data. Returns true if the data is correctly signed.
*
* #param publicKey
* public key associated with the developer account
* #param signedData
* signed data from server
* #param signature
* server signature
* #return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
Signature sig;
try {
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
byte[] decodedSig = Base64.decode(signature);
if (!sig.verify(decodedSig)) {
logger.error("Signature verification failed.");
return false;
}
return true;
}
catch (NoSuchAlgorithmException e) {
logger.error("NoSuchAlgorithmException.");
}
catch (InvalidKeyException e) {
logger.error("Invalid key specification.");
}
catch (SignatureException e) {
logger.error("Signature exception.");
}
catch (Base64DecoderException e) {
logger.error("Base64 decoding failed.");
}
return false;
}
}
for me maybe file encoding was the problem,
after changing eclipse workspace, it used mac file format again.
changing it to UTF-8 and copy&paste the key again into the project,
everything works fine now :/
wasted hours :/
Just an update. I never got to the bottom of why it stopped failing verification. We think it could be an issue with the Google Play servers and our Public Key.
Anyway the solution, as far as it is, is to implement the In-App Billing v3 Api (which is magnitudes nicer than the old version BTW) and it starting working again.
So, not really a definitive answer, but a fix as it were.