I have used
KeyChain.choosePrivateKeyAlias
I have successfully extracted the Private Key as well as the Public Key.
I want to handle the Click Listener of KeyChain, whether the user has allowed or deny the installation of certificates.
I couldnt find anything in the developers documentation.
Thank you
On Android ICS phone I have imported the PKCS#12 file containing private key and certificate. Then i run
KeyChain.choosePrivateKeyAlias(this, this, new String[] { "RSA" }, null, null, -1, null);
In the certificate selection dialog i choose the one just installed.
In the 'alias' callback i do the following:
public void alias(final String alias) {
...
protected Boolean[] doInBackground(Void... arg0) {
...
PrivateKey pk = KeyChain.getPrivateKey(ctx, alias);
Log.d(TAG, "EncodedPrivateKey: " + pk.toString());
And it gives me the full content of the private key.
Does it mean that any application, once allowed by user (in the cert. selection dialog), can read any private key installed from .pfx file?
Is the following scenario possible by standard Android means - "administrator" installing .pfx file with the cert.+private key and the permissions to read it are limited to the one specific app?
Related
I am building an Android App which communicates with my REST API that is protected by Spring Security.
Since the Android App is "public" and no keys etc is secure I want to create diffrent obstacles and make things complicated to protect my API as much as possible.
One way in which I would like to add more security is to make sure that the one calling my API has a certificate. I don't want to create thousands of certificates in my APIs trust-store so I just want to make sure that the caller have one single certificate that I hid away in a keystore in my Android app.
In the examples I have found it seems like a "normal" X509Certificate authentication in Spring Security requires a unique certificate for every user and then this certificate replaces Basic auth or JWT auth. I would like to have individual client JWT tokens but make sure that every call brings my ONE Android App certificate to make (more) sure that someone is calling my API from my Android app.
Is this possible or is it just not what it is for?
When you create a RestTemplate you can configure it with a keystore and trust-store so in that end it should be easy. But as for protecting my REST API it seems more difficult since I want both certificate + JWT token or Basic auth.
I am not using XML configuration for my securityconfig. I instead extend WebSecurityConfigurerAdapter. It would be great if this was configurable in the configure(HttpSecurity http) method, but I'm thinking that maybe I could achieve this in a OncePerRequestFilter somehow? Perhaps configure a filter before my JwtAuthFilter?
Edit:
In all the examples I have found for configuration of spring security they always seems to use the certificate as an authentication. I just want to configure so that when someone call example.com/api/** it checks so that the certificate is approved by my custom trust store (so that I "know" it is probably a call from my app) but if someone call example.com/website it should use the default java trust store.
If someone call example.com/api/** I would like my server to
check certificate and kill the connection if the certificate is not approved in my custom truststore.
If certificate is ok, establish https (or move on if I can't kill the connection before it have already established https-connection) to user auth with Basic-/JWT-authentication.
I think I figured it out. Here is how I configured it and it seems to work.
The "/**" endpoint is the website which should work with any browser without any specific certificate, but it requires Admin authority (you need to login as admin).
The "/api/**" and "/connect/**" endpoints require the correct certificate, the correct API-key and valid Basic- or JWT-token authentification.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/loginForm")
.loginProcessingUrl("/authenticateTheUser")
.permitAll()
.and()
.logout()
.permitAll().and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
http.requestMatchers()
.antMatchers("/connect/**","/api/**")
.and()
.addFilterBefore(new APIKeyFilter(null), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JwtAuthorizationFilter(), BasicAuthenticationFilter.class)
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/connect/**").hasAnyRole("MASTER,APPCALLER,NEGOTIATOR,MEMBER")
.antMatchers("/api/**").hasAnyRole("MASTER,MEMBER,ANONYMOUS");
}
The ApiKeyFilter class is the one that check the api-key and also make sure that the certificate used in the call is approved in my server trust-store. The api-key check is all that I had to configure, the extended X509AuthenticationFilter will automatically check the request certificate. My ApiKeyFilter looks like this:
public class APIKeyFilter extends X509AuthenticationFilter {
private String principalRequestHeader = "x-api-key";
private String apiKey = "XXXX";
public APIKeyFilter(String principalRequestHeader) {
if (principalRequestHeader != null) {
this.principalRequestHeader = principalRequestHeader;
}
setAuthenticationManager(new AuthenticationManager() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if(authentication.getPrincipal() == null) {
throw new BadCredentialsException("Access Denied.");
}
String rApiKey = (String) authentication.getPrincipal();
if (authentication.getPrincipal() != null && apiKey.equals(rApiKey)) {
return authentication;
} else {
throw new BadCredentialsException("Access Denied.");
}
}
});
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
return request.getHeader(principalRequestHeader);
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
X509Certificate[] certificates = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
if (certificates != null && certificates.length > 0) {
return certificates[0].getSubjectDN();
}
return super.getPreAuthenticatedCredentials(request);
}
}
Cred goes to these resources that helped me put things together:
Spring Boot - require api key AND x509, but not for all endpoints
spring security http antMatcher with multiple paths
I am using Android's keystore to implement fingerprint unlock of my Android app. I therefore use KeyGenerator to create a key using
var _keyGen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, "AndroidKeyStore")
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(GetAlias(_keyId),
KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
.SetBlockModes(KeyProperties.BlockModeCbc)
// Require the user to authenticate with biometry to authorize every use
// of the key
.SetEncryptionPaddings(KeyProperties.EncryptionPaddingPkcs7)
.SetUserAuthenticationRequired(true);
_keyGen.Init(
builder
.Build());
_keyGen.GenerateKey();
When I later enumerate the aliases in the store I find the key I have created:
_keystore.Load(null);
var aliases = _keystore.Aliases();
if (aliases == null)
{
og("KS: no aliases");
}
else
{
while (aliases.HasMoreElements)
{
var o = aliases.NextElement();
Log("alias: " + o?.ToString());
}
}
While this is working reliably on most devices, some devices (e.g. Google Pixel 4a) seem to "lose" the keys in the Keystore quite regularly. When enumerating the aliases as above, no key is listed anymore. I can reproduce this behavior by updating my app using a debugger (settings are such that SharedPreferences and app data are kept and I do not have this behavior on another device).
Is there anything I can do to prevent losing the keys?
I am developing an Android project.
I have a PEM certificate string:
-----BEGIN CERTIFICATE-----
MIIEczCCA1ugAwIBAgIBADANBgkqhkiG9w0BAQQFAD..AkGA1UEBhMCR0Ix
EzARBgNVBAgTClNvbWUtU3RhdGUxFDASBgNVBAoTC0..0EgTHRkMTcwNQYD
VQQLEy5DbGFzcyAxIFB1YmxpYyBQcmltYXJ5IENlcn..XRpb24gQXV0aG9y
...MANY LINES...
It8una2gY4l2O//on88r5IWJlm1L0oA8e4fR2yrBHX..adsGeFKkyNrwGi/
7vQMfXdGsRrXNGRGnX+vWDZ3/zWI0joDtCkNnqEpVn..HoX
-----END CERTIFICATE-----
(assigned above certificate string to a variable named CERT_STR)
I decode above PEM string to byte array:
byte[] pemBytes = Base64.decode(
CERT_STR.replaceAll("-----(BEGIN|END) CERTIFICATE-----", "")
.replaceAll("\n", "")
.getBytes("UTF-8"),
Base64.DEFAULT
);
I try to programmatically install the PEM certificate to my Android phone by following code:
Intent intent = KeyChain.createInstallIntent();
// because my PEM only contains a certificate, no private key, so I use EXTRA_CERTIFICATE
intent.putExtra(KeyChain.EXTRA_CERTIFICATE, pemBytes);// above PEM bytes
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
When run my code (in Android 7 device), the Android system certificate installer app pops up the window, when I press "OK" button of that window, I got following log:
java.io.IOException: stream does not represent a PKCS12 key store
at com.android.org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi.engineLoad(PKCS12KeyStoreSpi.java:793)
at java.security.KeyStore.load(KeyStore.java:1247)
at com.android.certinstaller.CredentialHelper.loadPkcs12Internal(CredentialHelper.java:396)
at com.android.certinstaller.CredentialHelper.extractPkcs12Internal(CredentialHelper.java:364)
at com.android.certinstaller.CredentialHelper.extractPkcs12(CredentialHelper.java:354)
at com.android.certinstaller.CertInstaller$1.doInBackground(CertInstaller.java:328)
at com.android.certinstaller.CertInstaller$1.doInBackground(CertInstaller.java:327)
My questions:
I have used EXTRA_CERTIFICATE & set it to intent, I am NOT using EXTRA_PKCS12, but from the log, Android system thinks I am installing PKCS#12 keystore. Why?
What is the correct way to programmatically install PEM certificate in Android?
Your code should work, as said #Sergey Nikitin. This starred example at Github is using similar code
I have reviewed the Android 7.1 source code of CredentialHelper and CertInstaller to trace your exception log. The unique reachable path to execute the pkcs12 loader at
com.android.certinstaller.CredentialHelper.extractPkcs12(CredentialHelper.java:354)
is the method onScreenlockOk
private void onScreenlockOk() {
if (mCredentials.hasPkcs12KeyStore()) {
if (mCredentials.hasPassword()) {
showDialog(PKCS12_PASSWORD_DIALOG);
} else {
new Pkcs12ExtractAction("").run(this);
}
which is protected by CredentialHelper.hasPkcs12KeyStore()
boolean hasPkcs12KeyStore() {
return mBundle.containsKey(KeyChain.EXTRA_PKCS12);
}
I have not found default assigned values or alternative paths, so I deduce that KeyChain.EXTRA_PKCS12 is being used in some way. It is a weird behaviour, may be you have a clean&rebuild issue?
I suggest to debug the code including Android CertInstaller class to ensure the values of the Extras and ensure that the executed code is the expected
I am trying to search on XMPP. I got the code from here. It works fine and I am able to connect to the server. But its showing the alert window like this
and If I click "Always" or "Once" it is accepting and I am able to show the contacts and chat messages....
Is there any way to stop this alert and can I connect directly to the server?
This message is displayed by MemorizingTrustManager (MTM), an Android library aimed to improve the security/usability trade-off for "private cloud" SSL installations.
MTM issues this warning whenever you connect to a server with a certificate not issued by one of the Android OS trusted Root CAs, like a self-signed certificate or one by CACert.
If the message appears again after you clicked "Always", this is a bug in MTM (probably due to a mismatching SSL server name), and should be reported via github.
Edit: if you are making an app that only communicates with one server, and you know the server's certificate in advance, you should replace MTM with AndroidPinning, which ensures that nobody can make man-in-the-middle attacks on your connection.
Disclaimer: I am the author of MTM and the mainainer of yaxim.
Get the certificate signed by a certificate authority. Forget all coding solutions.
Is there any way to stop this alert and can I connect directly to the server?
Its not clear to me if you wrote this app that connects to kluebook.com. I think you did, but its not explicit.
Assuming you wrote the app and know the server you are connecting to (kluebook.com), you should provide a custom TrustManager to handle this. You can find code for a custom TrustManger that works with the expected server certificate in OWASP's Certificate and Public Key Pinning example. Its OK to pin because you know what the certificate or public key is, and there's no need to trust someone else like a CA.
If you have no a priori knowledge, then you trust on first use and follow with a key continuity strategy looking for abrupt changes in the certificate or public key. In this case, the trust on first use should include the customary X509 checks.
Pinning is part of an overall security diversification strategy. A great book on the subject is Peter Gutmann's Engineering Security.
What you are seeing with the prompt is one leg of the strategy - namely, the Trust-On-First-Use (TOFU) strategy. The prompt has marginal value because user's don't know how to respond to it, so they just click yes to dismiss the box so they can continue what they are doing. Peter Gutmann has a great write-up on user psychology (complete with Security UI studies) in Engineering Security.
From section 6.1 of OWASP's Certificate and Public Key Pinning:
public final class PubKeyManager implements X509TrustManager
{
private static String PUB_KEY = "30820122300d06092a864886f70d0101...";
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
{
if (chain == null) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate array is null");
}
if (!(chain.length > 0)) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty");
}
if (!(null != authType && authType.equalsIgnoreCase("RSA"))) {
throw new CertificateException("checkServerTrusted: AuthType is not RSA");
}
// Perform customary SSL/TLS checks
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init((KeyStore) null);
for (TrustManager trustManager : tmf.getTrustManagers()) {
((X509TrustManager) trustManager).checkServerTrusted(chain, authType);
}
} catch (Exception e) {
throw new CertificateException(e);
}
// Hack ahead: BigInteger and toString(). We know a DER encoded Public Key begins
// with 0x30 (ASN.1 SEQUENCE and CONSTRUCTED), so there is no leading 0x00 to drop.
RSAPublicKey pubkey = (RSAPublicKey) chain[0].getPublicKey();
String encoded = new BigInteger(1 /* positive */, pubkey.getEncoded()).toString(16);
// Pin it!
final boolean expected = PUB_KEY.equalsIgnoreCase(encoded);
if (!expected) {
throw new CertificateException("checkServerTrusted: Expected public key: "
+ PUB_KEY + ", got public key:" + encoded);
}
}
}
}
You can get the expected public key from OpenSSL's s_client, but you have to know the port. I can't get a response from the well known SSL ports like 5223, and 5222 has no security services:
$ openssl s_client -connect kluebook.com:5222
CONNECTED(00000003)
140735088755164:error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol:s23_clnt.c:766:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 322 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
---
Once you get a public key, plug it back into the TrustManager at PUB_KEY.
I have an Android application with GAE server. I tried to authenticate the user as described on developers.google.com, I added the user parameter to the endpoint methods etc. I get a User which is not null, but this method getUserId() returns null. It is similar to this, rather old problem:
Function User.getUserId() in Cloud endpoint api returns null for a user object that is not null
But I still don't know how to work around it. How do you handle this error? Have you ever encountered it?
In android client here's what I did (its simplified) :
credentials = GoogleAccountCredential.usingAudience(getApplicationContext(), "server:client_id:" + WEB_CLIENT_ID);
credentials.setSelectedAccountName(accountName);
WarriorEntityEndpoint.Builder endpointBuilder = new WarriorEntityEndpoint.Builder(AndroidHttp.newCompatibleTransport(), new GsonFactory(), credentials);
warriorEntityEndpoint = endpointBuilder.build();
new AsyncTask<Void, Void, Void>() {
#Override
protected Void doInBackground(Void... params) {
// TODO Auto-generated method stub
try {
warriorEntityEndpoint.getWarrior().execute();
} catch (Exception e) {
}
return null;
}
}.execute();
And on GAE:
#Api(name = "warriorEntityEndpoint", namespace = #ApiNamespace(ownerDomain = "szpyt.com", ownerName = "szpyt.com", packagePath = "mmorpg.monsters"),
version = "version1",
scopes = {"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
clientIds = {Constants.ANDROID_CLIENT_ID, Constants.WEB_CLIENT_ID},
audiences = {Constants.ANDROID_AUDIENCE})
public class WarriorEntityEndpoint {
private static final Logger log = Logger.getLogger(WarriorEntityEndpoint.class.getName());
#ApiMethod(name = "getWarrior")
public WarriorEntity getWarrior(User user) throws OAuthRequestException, IOException {
log.log(Level.SEVERE, "this gives correct email: " + user.getEmail());
log.log(Level.SEVERE, "this is null: " + user.getUserId());
I have also another very important question: is this user authenticated, if getMail() gives me correct account, but getUserId() gives null? I read that user object should be null if it was not authenticated but I am not sure any more...
I'm using App engine SDK 1.8.7. I'm testing on a real device and backend deployed to GAE.
I asked the same question a while ago and got an answer. See link:
Function User.getUserId() in Cloud endpoint api returns null for a user object that is not null
The cause is a bug on appengine.
I guess there is no good solution for it right now. I store e-mail as a normal property and remove it from default fetch group, I use long as a primary key (generated by AppEngine) and I query the entity by the e-mail property. I don't like my solution, I'll accept ( and implement :) ) a better one if anyone can provide.
This is a known issue which has been filed with google, I've attached the issue link below.
There are two workarounds (1) save the user and read back from the store, if it refers to a valid account the user id will be populated (this sucks because you pay the saving / loading / deletion cost for each API access that is authenticated even if it is tiny, and obviously some performance cost) and (2) you could use the google+ ID but that is NOT the same as the user id.
This is extremely frustrating and there is currently no ETA as they are working on some fundamental issues with the auth design as far as I understand.
Please, vote for that issue by starring it. You can find all the information here
https://code.google.com/p/googleappengine/issues/detail?can=2&start=0&num=100&q=&colspec=ID%20Type%20Component%20Status%20Stars%20Summary%20Language%20Priority%20Owner%20Log&groupby=&sort=&id=8848
And here is the current formally approved workaround [(1) above], which you can also find in the link above, but for ease it's here: How can I determine a user_id based on an email address in App Engine?
For workaround (2) mentioned above, you can look at the first link, and go to post #39.