I'm trying to use Scribe to use Atlassian Jira using example from here:
https://developer.atlassian.com/display/JIRADEV/JIRA+REST+API+Example+-+OAuth+authentication
Anyone have any luck?
Here's what I got:
public class JiraAPI extends DefaultApi10a {
static final String BASE = "http://xasdf:8080/plugins/servlet";
#Override
public String getAccessTokenEndpoint()
{
return BASE + "/oauth/access-token";
}
#Override
public String getAuthorizationUrl(Token requestToken)
{
return BASE + "/oauth/authorize?oauth_token="+requestToken.getToken();
}
#Override
public String getRequestTokenEndpoint()
{
return BASE + "/oauth/request-token";
}
#Override
public SignatureService getSignatureService() {
return new RSASha1SignatureService(getPrivateKey());
}
private static PrivateKey getPrivateKey()
{
String str = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMPQ5BCMxlUq2TYy\n"+
"iRIoEUsz6HGTJhHuasS2nx1Se4Co3lxwxyubVdFj8AuhHNJSmJvjlpbTsGOjLZpr\n"+
"HyDEDdJmf1Fensh1MhUnBZ4a7uLrZrKzFHHJdamX9pxapB89vLeHlCot9hVXdrZH\n"+
"nNtg6FdmRKH/8gbs8iDyIayFvzYDAgMBAAECgYA+c9MpTBy9cQsR9BAvkEPjvkx2\n"+
"XL4ZnfbDgpNA4Nuu7yzsQrPjPomiXMNkkiAFHH67yVxwAlgRjyuuQlgNNTpKvyQt\n"+
"XcHxffnU0820VmE23M+L7jg2TlB3+rUnEDmDvCoyjlwGDR6lNb7t7Fgg2iR+iaov\n"+
"0iVzz+l9w0slRlyGsQJBAPWXW2m3NmFgqfDxtw8fsKC2y8o17/cnPjozRGtWb8LQ\n"+
"g3VCb8kbOFHOYNGazq3M7+wD1qILF2h/HecgK9eQrZ0CQQDMHXoJMfKKbrFrTKgE\n"+
"zyggO1gtuT5OXYeFewMEb5AbDI2FfSc2YP7SHij8iQ2HdukBrbTmi6qxh3HmIR58\n"+
"I/AfAkEA0Y9vr0tombsUB8cZv0v5OYoBZvCTbMANtzfb4AOHpiKqqbohDOevLQ7/\n"+
"SpvgVCmVaDz2PptcRAyEBZ5MCssneQJAB2pmvaDH7Ambfod5bztLfOhLCtY5EkXJ\n"+
"n6rZcDbRaHorRhdG7m3VtDKOUKZ2DF7glkQGV33phKukErVPUzlHBwJAScD9TqaG\n"+
"wJ3juUsVtujV23SnH43iMggXT7m82STpPGam1hPfmqu2Z0niePFo927ogQ7H1EMJ\n"+
"UHgqXmuvk2X/Ww==";
try
{
KeyFactory fac = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(str.getBytes()));
return fac.generatePrivate(privKeySpec);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}
Scribe has always been easy with other OAuth based providers, but this provider always "oauth_problem=signature_invalid" from debug:
generating signature...
base string is: POST&http%3A%2F%2Fxasdf%3A8080%2Fplugins%2Fservlet%2Foauth%2Frequest-token&oauth_callback%3Doob%26oauth_consumer_key%3Dhardcoded-consumer%26oauth_nonce%3D1556398454%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1357151719%26oauth_version%3D1.0
signature is: AJugUeZGup5dZvjNjx6bec6OrAszZVK+pMrTzahZbbzKzGkbli7okBy2KO5ww+OtqqnHWRgyzfWnQ0k6R5U0JzjR4QiOANJuwV8Un1NTZsrK32daefCp2uZ6W2d2Y/fmIl3toyCjAx41c3oJ78572vVFmBGihHUTUOYTlFP1X3M=
appended additional OAuth parameters: { oauth_callback -> oob , oauth_signature -> AJugUeZGup5dZvjNjx6bec6OrAszZVK+pMrTzahZbbzKzGkbli7okBy2KO5ww+OtqqnHWRgyzfWnQ0k6R5U0JzjR4QiOANJuwV8Un1NTZsrK32daefCp2uZ6W2d2Y/fmIl3toyCjAx41c3oJ78572vVFmBGihHUTUOYTlFP1X3M= , oauth_version -> 1.0 , oauth_nonce -> 1556398454 , oauth_signature_method -> RSA-SHA1 , oauth_consumer_key -> hardcoded-consumer , oauth_timestamp -> 1357151719 }
using Http Header signature
sending request...
response status code: 401
Exception in thread "main" org.scribe.exceptions.OAuthException: Response body is incorrect. Can't extract token and secret from this: 'oauth_signature=AJugUeZGup5dZvjNjx6bec6OrAszZVK%2BpMrTzahZbbzKzGkbli7okBy2KO5ww%2BOtqqnHWRgyzfWnQ0k6R5U0JzjR4QiOANJuwV8Un1NTZsrK32daefCp2uZ6W2d2Y%2FfmIl3toyCjAx41c3oJ78572vVFmBGihHUTUOYTlFP1X3M%3D&oauth_signature_base_string=POST%26http%253A%252F%252Ftracker%253A8080%252Fplugins%252Fservlet%252Foauth%252Frequest-token%26oauth_callback%253Doob%2526oauth_consumer_key%253Dhardcoded-consumer%2526oauth_nonce%253D1556398454%2526oauth_signature_method%253DRSA-SHA1%2526oauth_timestamp%253D1357151719%2526oauth_version%253D1.0&oauth_problem=signature_invalid&oauth_signature_method=RSA-SHA1'
response body: oauth_signature=AJugUeZGup5dZvjNjx6bec6OrAszZVK%2BpMrTzahZbbzKzGkbli7okBy2KO5ww%2BOtqqnHWRgyzfWnQ0k6R5U0JzjR4QiOANJuwV8Un1NTZsrK32daefCp2uZ6W2d2Y%2FfmIl3toyCjAx41c3oJ78572vVFmBGihHUTUOYTlFP1X3M%3D&oauth_signature_base_string=POST%26http%253A%252F%252Ftracker%253A8080%252Fplugins%252Fservlet%252Foauth%252Frequest-token%26oauth_callback%253Doob%2526oauth_consumer_key%253Dhardcoded-consumer%2526oauth_nonce%253D1556398454%2526oauth_signature_method%253DRSA-SHA1%2526oauth_timestamp%253D1357151719%2526oauth_version%253D1.0&oauth_problem=signature_invalid&oauth_signature_method=RSA-SHA1
at org.scribe.extractors.TokenExtractorImpl.extract(TokenExtractorImpl.java:41)
As far as I can tell, you cannot use Scribe with JIRA/Atlassian OAuth, since Scribe does not support RSA-SHA1 signatures, but this is the only Signature available in Atlassian's OAuth providers.
The only Java library, that I could find, which supports this method is the one used in the example code linked in the questions - the net.oauth one.
I have this working using scribe 1.3.6 using the provided RSASha1SignatureService, here's how I'm extracting the private key (from a PKCS#8 PEM file, the one with the BEGIN PRIVATE KEY header, not the BEGIN RSA PRIVATE KEY, which is the SSLeay format)
package com.company.client.api;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import org.scribe.builder.api.DefaultApi10a;
import org.scribe.builder.api.DefaultApi20;
import org.scribe.model.OAuthConfig;
import org.scribe.model.Token;
import org.scribe.services.RSASha1SignatureService;
import org.scribe.services.SignatureService;
public class JiraApi extends DefaultApi10a {
private static final String AUTHORIZE_URL = "http://localhost:8080/plugins/servlet/oauth/authorize?oauth_token=%s";
private static final String REQUEST_TOKEN_RESOURCE = "http://localhost:8080/plugins/servlet/oauth/request-token";
private static final String ACCESS_TOKEN_RESOURCE = "http://localhost:8080/plugins/servlet/oauth/access-token";
#Override
public String getAccessTokenEndpoint() {
return ACCESS_TOKEN_RESOURCE;
}
#Override
public String getRequestTokenEndpoint() {
return REQUEST_TOKEN_RESOURCE;
}
#Override
public String getAuthorizationUrl(Token requestToken) {
return String.format(AUTHORIZE_URL, requestToken.getToken());
}
#Override
public SignatureService getSignatureService() {
return new RSASha1SignatureService(getPrivateKey());
}
private PrivateKey getPrivateKey() {
try {
byte[] key = Base64.getDecoder().decode(JiraClientImpl.privateKey); // this is the PEM encoded PKCS#8 private key
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(keySpec);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Related
Currently working on upgrading from Java 13 to Java 16 and using the following updated dependencies for a cryptography library performing encryption/decryption:
BouncyCastle 1.69
Google Tink 1.6.1
=================================
Crypto Library class:
package com.decryption.test;
#NoArgsConstructor
#Service
public class CryptoLib {
protected static final String PROTOCOL_VERSION = "ECv2";
protected final String DEV_ROOT_PUB_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESXy7kqanQwAM/HBTcV0MVtgQkKLY6UVqP3Z/vdlxiRgFqnc9dZUSD8muUpgeZDD05lM68qoI31mbeX9c8P/9Uw==";
protected static final String MERCHANT_ID_PREFIX = "merchant:";
protected static final String HMAC_SHA256_ALGO = "HmacSha256";
protected static final int HMAC_SHA256_ALGO_SIZE = 256 / 8; // 256 bit key size -> 32 bytes
protected static final String AES_CTR_ALGO = "AES/CTR/NoPadding";
protected static final int AES_CTR_ALGO_SIZE = 256 / 8; // 256 bit key size -> 32 bytes
protected static final byte[] HKDF_EMPTY_SALT = new byte[0];
protected static final byte[] AES_CTR_ZERO_IV = new byte[16];
protected final static String ASYMMETRIC_KEY_TYPE = "EC";
protected static final String SENDER_ID = "Google";
protected static final String SECURITY_PROVIDER = "BC";
protected static final String EC_CURVE = "prime256v1";
protected static final String CSR_SIGNATURE = "SHA256WITHECDSA";
protected static final String SIGNATURE_ALGORITHM = "SHA256withECDSA";
#Autowired
private ProfileManager profileManager;
#PostConstruct
public void loadKeys() {
final StringBuilder message = new StringBuilder("Loading Google pay signing keys. ");
if (this.profileManager.isProfileActive("dev")) {
message.append("Profile DEV is active. Only test rig encrypted payloads supported.");
} else {
final GooglePaymentsPublicKeysManager googleKeyManager = this.getGoogleKeyManager();
googleKeyManager.refreshInBackground();
}
System.out.println(message);
}
private static String getMerchantIdComponent(final String merchantId) {
return merchantId.startsWith(MERCHANT_ID_PREFIX) ?
merchantId : MERCHANT_ID_PREFIX + merchantId;
}
private GooglePaymentsPublicKeysManager getGoogleKeyManager() {
return profileManager.isProfileActive("prod") ?
GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION :
GooglePaymentsPublicKeysManager.INSTANCE_TEST;
}
public String decrypt(final GoogleDecryptRequest decryptRequest) throws Exception {
try {
final PaymentMethodTokenRecipient.Builder builder = new PaymentMethodTokenRecipient.Builder()
.protocolVersion(PROTOCOL_VERSION)
.recipientId(getMerchantIdComponent(decryptRequest.getMerchantId()));
if (decryptRequest.getPrivateKey() != null) {
builder.addRecipientPrivateKey(decryptRequest.getPrivateKey());
}
//Add all our retired keys to the list.
for (final ECPrivateKey ecPrivateKey : decryptRequest.getOldPrivateKeys()) {
builder.addRecipientPrivateKey(ecPrivateKey);
}
if (this.profileManager.isProfileActive("dev")) {
builder.addSenderVerifyingKey(DEV_ROOT_PUB_KEY);
} else {
builder.fetchSenderVerifyingKeysWith(getGoogleKeyManager());
}
return builder.build()
.unseal(decryptRequest.getEncryptedMessage());
} catch (final Exception e) {
final String errMsg = MessageFormat.format("GooglePay Decryption failed for Google merchant ID [{0}]", decryptRequest.getMerchantId());
throw new Exception(errMsg, e);
}
}
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
static class GoogleEncryptRequest implements EncryptRequest<ECPublicKey> {
/**
* Merchant public key
*/
private ECPublicKey publicKey;
private String algorithm = "EC";
/**
* Google merchantID. If the merchant ID is xxx, this value should be either
* <ul>
* <li>xxx</li>
* <li>merchant:xxx</li>
* </ul>
*/
private String merchantId;
/**
* Message to encrypt
*/
private String message;
}
public EncryptResponse encrypt(final GoogleEncryptRequest encryptRequest) throws Exception {
final String DEV_ROOT_PRIV_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg3lO35/XiUzEooUJlLKEd0BJmoLgeTvkFSm/b4wMrgdWgCgYIKoZIzj0DAQehRANCAARJfLuSpqdDAAz8cFNxXQxW2BCQotjpRWo/dn+92XGJGAWqdz11lRIPya5SmB5kMPTmUzryqgjfWZt5f1zw//1T";
final String DEV_ROOT_PUB_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESXy7kqanQwAM/HBTcV0MVtgQkKLY6UVqP3Z/vdlxiRgFqnc9dZUSD8muUpgeZDD05lM68qoI31mbeX9c8P/9Uw==";
final GoogleEncryptResponse response = new GoogleEncryptResponse();
try {
//STEP 1: Generate all keys.
// Generate root keypair. Used to sign intermediate. jsonPath -> $.intermediateSigningKey.signatures
final PrivateKey rootPrivKey = CryptoUtility.getPrivateFromPKCS8Base64(DEV_ROOT_PRIV_KEY, ASYMMETRIC_KEY_TYPE);
final PublicKey rootPubKey = CryptoUtility.getPublicFromBase64(DEV_ROOT_PUB_KEY, ASYMMETRIC_KEY_TYPE); //equiv to the verifying key from the test/prod APIs
// Generate intermediate keypair. Used to sign whole payload. jsonPath -> $.signature
final KeyPair intermediateKeyPair = generateKeyPair();
final PrivateKey intermediatePrivKey = intermediateKeyPair.getPrivate();
// Convert to uncompressed point and B64 encode. Goes to $.intermediateSigningKey.signedKey.keyValue
final PublicKey intermediatePubKey = intermediateKeyPair.getPublic();
// Generate ephemeral pub key. Used to create a shared secret to decrypt the signed message
final KeyPair ephemeralKeyPair = generateKeyPair();
final PrivateKey ephemeralPrivKey = ephemeralKeyPair.getPrivate();
// Convert to uncompressed point and B64 encode. Goes to $.signedMessage.ephemeralPublicKey
final PublicKey ephemeralPubKey = ephemeralKeyPair.getPublic();
final byte[] ephemeralPubKeyBytes = getUncompressedPoint(ephemeralPubKey);
//Parse merchant public key
final ECPublicKey merchantPublicKey = encryptRequest.getPublicKey();
/*
STEP 2: Sign the intermediate key. This step will generate the intermediateSigningKey block in the json
ECDSA(length_of_sender_id || sender_id || length_of_protocol_version || protocol_version || length_of_signed_key || signed_key)
*/
final long keyExpiry = Instant.now().plus(365, ChronoUnit.DAYS).toEpochMilli();
final String intermediatePubKeyB64 = java.util.Base64.getEncoder().encodeToString(intermediatePubKey.getEncoded());
response.setIntermediateSigningKey(keyExpiry, intermediatePubKeyB64);
final byte[] senderComponent = getSignatureBytesForComponent(SENDER_ID);
final byte[] protocolVersionComponent = getSignatureBytesForComponent(PROTOCOL_VERSION);
final String signingKeyJson = new Gson().toJson(response.getIntermediateSigningKey().getSignedKey());
final byte[] signingKeyComponent = getSignatureBytesForComponent(signingKeyJson);
//Assemble all components into one byte array
final byte[] intermediateSignatureComponentBytes = ByteBuffer
.allocate(senderComponent.length + protocolVersionComponent.length + signingKeyComponent.length)
.put(senderComponent)
.put(protocolVersionComponent)
.put(signingKeyComponent)
.array();
//Sign the byte array using ECDSA
final String intermediateSignature = ecdsaSignMessageB64(rootPrivKey, intermediateSignatureComponentBytes);
response.setIntermediateSigningKeySignatures(intermediateSignature);
/*
Step 3: Create the signed message. Includes tag, encrypted data and ephemeralPubKey
*/
//Step 3a: Generate shared secret. Used for symmetric encryption
final byte[] sharedSecret = EllipticCurves.computeSharedSecret(
(ECPrivateKey) ephemeralPrivKey,
merchantPublicKey.getW());
//Step 3b: Generate HMAC key and symmetric key using shared secret
final byte[] eciesKey = Hkdf.computeEciesHkdfSymmetricKey(
ephemeralPubKeyBytes,
sharedSecret,
HMAC_SHA256_ALGO,
HKDF_EMPTY_SALT,
SENDER_ID.getBytes(StandardCharsets.UTF_8),
HMAC_SHA256_ALGO_SIZE + AES_CTR_ALGO_SIZE //256 bit aes key and 256 bit hmac key
);
final byte[] aesKey = Arrays.copyOf(eciesKey, AES_CTR_ALGO_SIZE); // [0,32] bytes
final byte[] hmacKey = Arrays.copyOfRange(eciesKey, HMAC_SHA256_ALGO_SIZE, HMAC_SHA256_ALGO_SIZE * 2); //[32,64]
//Step 3c: Encrypt data
final Cipher cipher = EngineFactory.CIPHER.getInstance(AES_CTR_ALGO);
cipher.init(
Cipher.ENCRYPT_MODE,
new SecretKeySpec(aesKey, "AES"),
new IvParameterSpec(AES_CTR_ZERO_IV));
final byte[] cipherBytes = cipher.doFinal(encryptRequest.getMessage().getBytes(StandardCharsets.UTF_8));
//Step 3d: Compute HMAC tag
final SecretKeySpec secretKeySpec = new SecretKeySpec(hmacKey, HMAC_SHA256_ALGO);
final Mac mac = EngineFactory.MAC.getInstance(HMAC_SHA256_ALGO);
mac.init(secretKeySpec);
final byte[] hmacBytes = mac.doFinal(cipherBytes);
//Step 3e: Populate data in response. $.signedMessage
final String ephemeralPublicKeyB64 = java.util.Base64.getEncoder().encodeToString(ephemeralPubKeyBytes);
final String hmacB64 = java.util.Base64.getEncoder().encodeToString(hmacBytes);
final String cipherB64 = java.util.Base64.getEncoder().encodeToString(cipherBytes);
response.setSignedMessage(cipherB64, hmacB64, ephemeralPublicKeyB64);
/*
Step 4: Create $.signature using intermediate priv and the serialized signed key
ECDSA(length_of_sender_id || sender_id || length_of_recipient_id || recipient_id || length_of_protocolVersion || protocolVersion || length_of_signedMessage || signedMessage)
*/
final String merchantIdComponentString = getMerchantIdComponent(encryptRequest.getMerchantId());
final byte[] merchantComponent = getSignatureBytesForComponent(merchantIdComponentString);
;
final String signedMessageJson = new Gson().toJson(response.getSignedMessage());
final byte[] signedMessageComponent = getSignatureBytesForComponent(signedMessageJson);
//Assemble all components into one byte array
final int signatureComponentLength = senderComponent.length + merchantComponent.length + protocolVersionComponent.length + signedMessageComponent.length;
final byte[] signatureComponentBytes = ByteBuffer
.allocate(signatureComponentLength)
.put(senderComponent)
.put(merchantComponent)
.put(protocolVersionComponent)
.put(signedMessageComponent)
.array();
final String signatureB64 = ecdsaSignMessageB64(intermediatePrivKey, signatureComponentBytes);
response.setSignature(signatureB64);
} catch (final Exception e) {
throw new Exception("Encryption failed: " + e);
}
return response;
}
static byte[] getSignatureBytesForComponent(final String component) {
final byte[] componentLengthBytes = ByteBuffer
.allocate(4)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt(component.length())
.array();
final byte[] componentBytes = ByteBuffer
.wrap(component.getBytes(StandardCharsets.UTF_8))
.array();
return ByteBuffer.allocate(componentLengthBytes.length + componentBytes.length)
.put(componentLengthBytes)
.put(componentBytes)
.array();
}
public static KeyPair generateKeyPair() throws Exception {
try {
final KeyPairGenerator kpg = KeyPairGenerator.getInstance(ASYMMETRIC_KEY_TYPE, SECURITY_PROVIDER);
kpg.initialize(generateECParameterSpec());
return kpg.generateKeyPair();
} catch (final GeneralSecurityException e) {
System.out.println("Failed to generate ECC KeyPair. " + e);
throw new Exception("Failed to generate ECC KeyPair.");
}
}
protected static ECNamedCurveSpec generateECParameterSpec() {
final ECNamedCurveParameterSpec bcParams = ECNamedCurveTable.getParameterSpec(EC_CURVE);
final ECNamedCurveSpec params = new ECNamedCurveSpec(bcParams.getName(), bcParams.getCurve(),
bcParams.getG(), bcParams.getN(), bcParams.getH(), bcParams.getSeed());
return params;
}
private static String ecdsaSignMessageB64(final PrivateKey privateKey, final byte[] messageToSign) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException {
final Signature intermediateECDSASignature = Signature.getInstance(SIGNATURE_ALGORITHM);
intermediateECDSASignature.initSign(privateKey);
intermediateECDSASignature.update(messageToSign);
final byte[] intermediateSignatureBytes = intermediateECDSASignature.sign();
return java.util.Base64.getEncoder().encodeToString(intermediateSignatureBytes);
}
public static byte[] getUncompressedPoint(final PublicKey publicKey) {
final java.security.interfaces.ECPublicKey pub = (java.security.interfaces.ECPublicKey) publicKey;
return pub.getEncoded();
}
#Component
public class ProfileManager {
#Autowired
private Environment environment;
#PostConstruct
public void setup() {
System.out.println(this.getActiveProfiles());
}
public boolean isProfileActive(final String profile) {
for (final String activeProfile : this.environment.getActiveProfiles()) {
if (activeProfile.equalsIgnoreCase(profile)) {
return true;
}
}
return false;
}
public List<String> getActiveProfiles() {
return List.of(this.environment.getActiveProfiles());
}
}
#Slf4j
#Data
#NoArgsConstructor
public class GoogleEncryptResponse implements EncryptResponse {
//These entities represent the structure provided by the GPay spec. See link in class docs
private String protocolVersion = "ECv2";
private String signature;
private IntermediateSigningKey intermediateSigningKey = new IntermediateSigningKey();
private SignedMessage signedMessage = new SignedMessage();
public void setIntermediateSigningKey(final long keyExpiration, final String keyValue) {
this.intermediateSigningKey.getSignedKey().setKeyExpiration(Long.toString(keyExpiration));
this.intermediateSigningKey.getSignedKey().setKeyValue(keyValue);
}
public void setIntermediateSigningKeySignatures(final String... signatures) {
this.intermediateSigningKey.setSignatures(signatures);
}
public void setSignedMessage(final String encryptedMessage, final String tag, final String ephemeralPublicKey) {
this.signedMessage.setEncryptedMessage(encryptedMessage);
this.signedMessage.setTag(tag);
this.signedMessage.setEphemeralPublicKey(ephemeralPublicKey);
}
#Override
public String getResult() {
Gson gson = new Gson();
JsonObject intermediateSigningKeyJson = new JsonObject();
JsonArray intermediateSigningKeyJsonSignatures = new JsonArray();
Arrays.stream(intermediateSigningKey.getSignatures())
.forEach(intermediateSigningKeyJsonSignatures::add);
intermediateSigningKeyJson.add("signatures", intermediateSigningKeyJsonSignatures);
intermediateSigningKeyJson.addProperty("signedKey", gson.toJson(getIntermediateSigningKey().getSignedKey()));
JsonObject payloadRoot = new JsonObject();
payloadRoot.addProperty("protocolVersion", protocolVersion);
payloadRoot.addProperty("signature", signature);
payloadRoot.add("intermediateSigningKey", intermediateSigningKeyJson);
payloadRoot.addProperty("signedMessage", gson.toJson(getSignedMessage()));
return getJsonElementHtmlSafe(payloadRoot);
}
#Data
#NoArgsConstructor
public static class IntermediateSigningKey {
private SignedKey signedKey = new SignedKey();
/**
* Signatures signed by the verifying key. Should be B64 encoded used SHA256 hashing with
* ECDSA
*/
private String[] signatures;
#Data
#NoArgsConstructor
static class SignedKey {
/**
* A Base64 version of key encoded in ASN.1 type.
* aka, der format. key.getEncoded()
*/
private String keyValue;
/**
* Date and time when the intermediate key expires as UTC milliseconds since epoch
*/
private String keyExpiration;
}
}
#Setter
#NoArgsConstructor
private static class SignedMessage {
/**
* A Base64-encoded encrypted message that contains payment information
*/
private String encryptedMessage;
/**
* A Base64-encoded ephemeral public key associated
* with the private key to encrypt the message in uncompressed point format
*/
private String ephemeralPublicKey;
/**
* A Base64-encoded MAC of encryptedMessage.
*/
private String tag;
}
}
public interface EncryptResponse {
String getResult();
}
public interface EncryptRequest<K extends PublicKey> {
/**
* Public key used to encrypt
* #return
*/
K getPublicKey();
void setPublicKey(K publicKey);
String getAlgorithm();
/**
* Set the PublicKey object using a base64 encoded key.
* #param base64EncodedKey
*/
default void setPublicKey(final String base64EncodedKey) throws Exception {
PublicKey publicKey = CryptoUtility.getPublicFromBase64(base64EncodedKey, getAlgorithm());
setPublicKey((K) publicKey);
}
/**
* Message to encrypt
* #return
*/
String getMessage();
}
public static String getJsonElementHtmlSafe(JsonElement element) {
try {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.setLenient(false);
jsonWriter.setHtmlSafe(true); //Ensures '=' will appear as \u003d
Streams.write(element, jsonWriter);
return stringWriter.toString();
} catch (IOException e) {
return null;
}
}
#Builder
#Data
#NoArgsConstructor
#AllArgsConstructor
public static class GoogleDecryptRequest implements DecryptRequest<ECPrivateKey> {
/**
* This represents the encrypted value
*/
private String encryptedMessage;
/**
* The current active private key
*/
private ECPrivateKey privateKey;
/**
* These privateKey represents a private key that may have been rotated recently.
* May be empty
*/
#Singular
private List<ECPrivateKey> oldPrivateKeys = new ArrayList<>();
/**
* Corresponds to the GPay console merchant id
* Value must be prefixed with merchant:<br/>
* For example: merchant:1234345345345
*/
private String merchantId;
private String algorithm = "EC";
public void addOldPrivateKey(final ECPrivateKey privateKey) {
this.oldPrivateKeys.add(privateKey);
}
public static class GoogleDecryptRequestBuilder {
public GoogleDecryptRequestBuilder encryptedMessage(final String encryptedMessage) {
try {
final JsonElement jsonPayload = new JsonParser().parse(encryptedMessage);
this.encryptedMessage = getJsonElementHtmlSafe(jsonPayload);
} catch (final Exception e) {
System.out.println("Failed to parse GooglePay encrypted message. Data MUST be a valid json string." + e);
}
return this;
}
}
}
public interface DecryptRequest<K extends PrivateKey> {
/**
* Return the message that will be decrypted.
* #return
*/
String getEncryptedMessage();
/**
* Return the private key required for decryption
* #return
*/
K getPrivateKey();
void setPrivateKey(K privateKey);
String getAlgorithm();
/**
* Set the PublicKey object using a base64 encoded key.
* #param base64EncodedKey
*/
default void setPrivateKey(final String base64EncodedKey) throws Exception {
PrivateKey privateKey = CryptoUtility.getPrivateFromPKCS8Base64(base64EncodedKey, getAlgorithm());
setPrivateKey((K) privateKey);
}
}
}
CryptoUtility class:
package com.decryption.test;
public class CryptoUtility {
public static PrivateKey getPrivateFromPKCS8Base64(final String base64, final String algorithm) throws Exception {
final byte[] privateKeyBytes = Base64.decode(base64);
return getPrivateFromPKCS8Der(privateKeyBytes, algorithm);
}
public static PrivateKey getPrivateFromPKCS8Der(final byte[] privateKeyBytes, final String algorithm) throws Exception {
try {
final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes);
final KeyFactory kf = KeyFactory.getInstance(algorithm, CryptoLib.SECURITY_PROVIDER);
return kf.generatePrivate(spec);
} catch (final Exception e) {
throw new Exception("Failed to created Private Key." + e);
}
}
public static PublicKey getPublicFromBase64(final String base64, final String algorithm) throws Exception {
final byte[] publicKeyBytes = Base64.decode(base64);
return getPublicFromDer(publicKeyBytes, algorithm);
}
public static PublicKey getPublicFromDer(final byte[] publicKeyBytes, final String algorithm) throws Exception {
try {
final X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKeyBytes, algorithm);
final KeyFactory kf = KeyFactory.getInstance(algorithm, CryptoLib.SECURITY_PROVIDER);
return kf.generatePublic(spec);
} catch (final Exception e) {
throw new Exception("Failed to created Public Key from string value" + e);
}
}
}
JUnit class:
package com.decryption.test;
#ExtendWith(MockitoExtension.class)
public class DecryptionTest {
#InjectMocks
private CryptoLib googleECCCryptoLibrary;
#Mock
private CryptoLib.ProfileManager profileManager;
#Test
void encryptAndDecryptMockData() throws Exception {
final ECPrivateKey merchantPrivKey = (ECPrivateKey) CryptoUtility.getPrivateFromPKCS8Base64("MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgsfylDy+Q3lfR6nbipZGDZWzp6P+qiQeApJizxG/hj96gCgYIKoZIzj0DAQehRANCAAQg1SVNuof6FndzJkbPst37moW+L/36EPLiiosS5BBSsLK4q2aLxzk2M732OpDHkXTp31ZQitPDQImndaY57ZSM", "EC");
final ECPublicKey merchantPubKey = (ECPublicKey) CryptoUtility.getPublicFromBase64("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEINUlTbqH+hZ3cyZGz7Ld+5qFvi/9+hDy4oqLEuQQUrCyuKtmi8c5NjO99jqQx5F06d9WUIrTw0CJp3WmOe2UjA==", "EC");
final CryptoLib.GoogleEncryptRequest encRequest = CryptoLib.GoogleEncryptRequest.builder()
.merchantId("12345678901234567890")
.message("heyhey!")
.publicKey(merchantPubKey)
.build();
final CryptoLib.EncryptResponse encResponse = googleECCCryptoLibrary.encrypt(encRequest);
final CryptoLib.GoogleDecryptRequest decryptRequest = CryptoLib.GoogleDecryptRequest.builder()
.privateKey(merchantPrivKey)
.merchantId("12345678901234567890")
.encryptedMessage(encResponse.getResult())
.build();
given(this.profileManager.isProfileActive(anyString())).willReturn(true);
final String decResponse = this.googleECCCryptoLibrary.decrypt(decryptRequest);
assertEquals(encRequest.getMessage(), decResponse);
}
}
Stacktrace:
Caused by: java.security.GeneralSecurityException: java.lang.IllegalStateException: java.security.InvalidAlgorithmParameterException: Curve not supported: secp256r1 [NIST P-256,X9.62 prime256v1] (1.2.840.10045.3.1.7)
at com.google.crypto.tink.subtle.EllipticCurves.computeSharedSecret(EllipticCurves.java:963)
ECDHKeyAgreement.engineGenerateSecret() implementation seems to be changed in Java 16 where it is now throwing:
throw new IllegalStateException(new InvalidAlgorithmParameterException("Curve not supported
as compared to the implementation in Java 13.
Any recommendations to implement encryption in JDK 16 with BouncyCastle as the provider or if it should be replaced by any other provider?
I created an account on https://developer.aife.economie.gouv.fr/ website and I want to try API on the sandbox. For this an application has been generated
For this application, I obtain API key and OAuth2 Credentials. Here are my previous API keys.
By reading the documentation, I have the following entry points for authentication
My objective is to get authenticated and get an auth token in order to consume this API. Here is my code:
package com.oauth.app;
import org.apache.oltu.oauth2.client.OAuthClient;
import org.apache.oltu.oauth2.client.URLConnectionClient;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
import org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse;
import org.apache.oltu.oauth2.common.OAuth;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
public class OAuthApp {
/**
* URL for requesting OAuth access tokens.
*/
private static final String TOKEN_REQUEST_URL =
"https://sandbox-oauth.aife.economie.gouv.fr/api/oauth/token";
/**
* Client ID of your client credential. Change this to match whatever credential you have created.
*/
private static final String CLIENT_ID =
"1f80aa43-e12f-4e1c-ad42-87ec16baf060";
/**
* Client secret of your client credential. Change this to match whatever credential you have created.
*/
private static final String CLIENT_SECRET =
"a232af0e-513e-4a64-9977-410d237dc421";
/**
* Account on which you want to request a resource. Change this to match the account you want to
* retrieve resources on.
*/
private static final String ACCOUNT_ID =
"a232af0e-513e-4a64-9977-410d237dc421";
/**
* Request a fresh access token using the given client ID, client secret, and token request URL,
* then request the resource at the given resource URL using that access token, and get the resource
* content. If an exception is thrown, print the stack trace instead.
*
* #param args Command line arguments are ignored.
*/
public static void main(String[] args) {
try {
OAuthClient client = new OAuthClient(new URLConnectionClient());
System.out.println("OAuthClient " + client.toString());
OAuthClientRequest request =
OAuthClientRequest.tokenLocation(TOKEN_REQUEST_URL)
.setGrantType(GrantType.CLIENT_CREDENTIALS)
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
// .setScope() here if you want to set the token scope
.buildQueryMessage();
request.addHeader("Accept", "application/json");
// request.addHeader("Content-Type", "application/json");
// request.addHeader("Authorization", base64EncodedBasicAuthentication());
System.out.println("OAuthClientRequest body\n\t " + request.getBody());
System.out.println("OAuthClientRequest headers\n\t " + request.getHeaders());
System.out.println("OAuthClientRequest locationUri\n\t " + request.getLocationUri());
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}};
// Install the all-trusting trust manager
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
String token = client.accessToken(
request,
OAuth.HttpMethod.GET,
OAuthJSONAccessTokenResponse.class).getAccessToken();
} catch (OAuthSystemException | OAuthProblemException e) {
e.printStackTrace();
}
}
}
I obtain this in my console:
OAuthClient org.apache.oltu.oauth2.client.OAuthClient#7e0ea639
OAuthClientRequest body
null
OAuthClientRequest headers
{Accept=application/json, Content-Type=application/json}
OAuthClientRequest locationUri
https://sandbox-oauth.aife.economie.gouv.fr/api/oauth/token?grant_type=client_credentials&client_secret=a232af0e-513e-4a64-9977-410d237dc421&client_id=42b214ec-7eaf-4f37-aeb5-ae91057a0e27
OAuthProblemException{error='unsupported_response_type', description='Invalid response! Response body is not application/json encoded', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}
at org.apache.oltu.oauth2.common.exception.OAuthProblemException.error(OAuthProblemException.java:63)
at org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse.setBody(OAuthJSONAccessTokenResponse.java:76)
at org.apache.oltu.oauth2.client.response.OAuthClientResponse.init(OAuthClientResponse.java:92)
at org.apache.oltu.oauth2.client.response.OAuthAccessTokenResponse.init(OAuthAccessTokenResponse.java:65)
at org.apache.oltu.oauth2.client.response.OAuthClientResponse.init(OAuthClientResponse.java:101)
at org.apache.oltu.oauth2.client.response.OAuthAccessTokenResponse.init(OAuthAccessTokenResponse.java:60)
at org.apache.oltu.oauth2.client.response.OAuthClientResponse.init(OAuthClientResponse.java:120)
at org.apache.oltu.oauth2.client.response.OAuthClientResponseFactory.createCustomResponse(OAuthClientResponseFactory.java:82)
at org.apache.oltu.oauth2.client.URLConnectionClient.execute(URLConnectionClient.java:111)
at org.apache.oltu.oauth2.client.OAuthClient.accessToken(OAuthClient.java:65)
at com.oauth.app.OAuthApp.main(OAuthApp.java:101)
I obtain this error message:
OAuthProblemException{error='unsupported_response_type', description='Invalid response! Response body is not application/json encoded'
I also tried to use a curl call to the API :
curl –k –H "content-type :application/x-www-form-urlencoded" –d "grant_type=client_credentials&client_id=42b214ec-7eaf-4f37-aeb5-ae91057a0e27&client_secret=a232af0e-513e-4a64-9977-410d237dc421&scope=openid" –X POST https://sandbox-oauth.aife.finances.rie.gouv.fr/api/oauth/token
curl: (6) Could not resolve host: -k
curl: (6) Could not resolve host: -H
curl: (3) Port number ended with 'a'
curl: (6) Could not resolve host: -d
curl: (6) Could not resolve host: grant_type=client_credentials&client_id=42b214ec-7eaf-4f37-aeb5-ae91057a0e27&client_secret=a232af0e-513e-4a64-9977-410d237dc421&scope=openid
curl: (6) Could not resolve host: -X
curl: (6) Could not resolve host: POST
curl: (6) Could not resolve host: sandbox-oauth.aife.finances.rie.gouv.fr
Ok i finally solved my own issue. There was no need to use OAuth stuff.
It's divided onto 2 classes. This code is just for testing purpose.
public class OAuthApp {
private static final String TOKEN_REQUEST_URL = "https://sandbox-oauth.aife.economie.gouv.fr/api/oauth/token";
private static final String CLIENT_ID = "xxxxxx";
private static final String CLIENT_SECRET = "xxxxxx";
private static final String GRANT_TYPE = "client_credentials";
private static final String SCOPE = "openid";
public static void main(String[] args) throws IOException {
try {
Map<String, String> headers = new HashMap<>();
HttpsPostForm httpsPostForm = new HttpsPostForm(TOKEN_REQUEST_URL, "utf-8", headers);
httpsPostForm.addFormField("grant_type", GRANT_TYPE);
httpsPostForm.addFormField("client_id", CLIENT_ID);
httpsPostForm.addFormField("client_secret", CLIENT_SECRET);
httpsPostForm.addFormField("scope", SCOPE);
// Result
String response = httpsPostForm.finish();
System.out.println(response);
} catch (IOException e) {
e.printStackTrace();
}
}
}
My second class is just building the HTTPS request and set the headers elements. The empty trust manager helps to avoid error messages.
public class HttpsPostForm {
private HttpsURLConnection conn;
private Map<String, Object> queryParams;
private String charset;
public HttpsPostForm(String requestURL, String charset, Map<String, String> headers, Map<String, Object> queryParams) throws IOException {
this.charset = charset;
if (queryParams == null) {
this.queryParams = new HashMap<>();
} else {
this.queryParams = queryParams;
}
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}};
// Install the all-trusting trust manager
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
URL url = new URL(requestURL);
conn = (HttpsURLConnection) url.openConnection();
conn.setUseCaches(false);
conn.setDoOutput(true); // indicates POST method
conn.setDoInput(true);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
if (headers != null && headers.size() > 0) {
Iterator<String> it = headers.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
String value = headers.get(key);
conn.setRequestProperty(key, value);
}
}
}
public HttpsPostForm(String requestURL, String charset, Map<String, String> headers) throws IOException {
this(requestURL, charset, headers, null);
}
public HttpsPostForm(String requestURL, String charset) throws IOException {
this(requestURL, charset, null, null);
}
public void addFormField(String name, Object value) {
queryParams.put(name, value);
}
public void addHeader(String key, String value) {
conn.setRequestProperty(key, value);
}
private byte[] getParamsByte(Map<String, Object> params) {
byte[] result = null;
StringBuilder postData = new StringBuilder();
for (Map.Entry<String, Object> param : params.entrySet()) {
if (postData.length() != 0) {
postData.append('&');
}
postData.append(this.encodeParam(param.getKey()));
postData.append('=');
postData.append(this.encodeParam(String.valueOf(param.getValue())));
}
try {
result = postData.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
private String encodeParam(String data) {
String result = "";
result = URLEncoder.encode(data, StandardCharsets.UTF_8);
return result;
}
public String finish() throws IOException {
String response = "";
byte[] postDataBytes = this.getParamsByte(queryParams);
conn.getOutputStream().write(postDataBytes);
// Check the http status
int status = conn.getResponseCode();
if (status == HttpsURLConnection.HTTP_OK) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = conn.getInputStream().read(buffer)) != -1) {
result.write(buffer, 0, length);
}
response = result.toString(this.charset);
conn.disconnect();
} else {
throw new IOException("Server returned non-OK status: " + status);
}
return response;
}
}
Finally I can print my Json string :
{
"access_token":"Js1NYJvtQREj0I0Dz5b0qrMh8gjJBlltJAit2Yx6BGJDloixPv2JwB",
"token_type":"Bearer",
"expires_in":3600,
"scope":"openid resource.READ"
}
I also had some difficulties with Chorus API but I achieve to get the tokenKey with that with the same method but buildBodyMessage() at the end.
// Création requête pour obtenir le token Oauth2 API CHORUS
request = OAuthClientRequest
.tokenLocation(urlToken)
.setGrantType(GrantType.CLIENT_CREDENTIALS)
.setClientId(clientid)
.setClientSecret(clientsecret)
.setScope(OidcScopes.OPENID)
.buildBodyMessage();
// Ajout du Cpro-account
request.addHeader("cpro-account", cproAccount);
tokenChorus = client.accessToken(request, OAuth.HttpMethod.POST, OAuthJSONAccessTokenResponse.class)
.getAccessToken();
that create token formated in String. And afterthat you must create HttpUrlConnection with this token with headers like that
HttpURLConnection connexion = null;
try {
URL url = new URL(currentUrl);
connexion = (HttpURLConnection) url.openConnection();
} catch (IOException e) {
e.printStackTrace();
}
connexion.setRequestProperty("Content-type", "application/json");
connexion.setRequestProperty("Authorization", "Bearer " + tokenChorus);
connexion.setRequestProperty("cpro-account", cproAccount);
try {
connexion.setRequestMethod("POST");
} catch (ProtocolException e) {
e.printStackTrace();
}
connexion.setDoInput(true);
connexion.setDoOutput(true);
return connexion;
I'm new to Netty and I try to write an echo server and client that uses mutual authentication. Unfortunately, it's not working, the client doesn't send its client certificate and the server disconnects as expected. Below an overview of what I've done so far and the client side code - that probably contains some bug or I missed something important. Thanks for going through all this!
That is what I have:
Netty version 4.1.0.CR1
Valid keystores, truststores and CRL for download on server
A complete implementation of echo server and client using JSSE directly (that is working as expected)
A working implementation of the echo server using Netty (it's working fine when used with the JSSE based client)
A client based on Netty that does not send a client certificate
Client code:
The channel handler:
package info.junius.tutorial.echo.netty.tls;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf>
{
#Override
public void channelRead0(ChannelHandlerContext ctx, ByteBuf in)
{
System.out.println("CLIENT: Received echo from server:\n" + in.toString(CharsetUtil.UTF_8));
}
#Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
{
cause.printStackTrace();
ctx.close();
}
}
The channel initialiser:
package info.junius.tutorial.echo.netty.tls;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.ssl.SslContext;
public class ClientChannelInitializer extends ChannelInitializer<Channel>
{
private final SslContext context;
private final String peerHost;
private final int peerPort;
public ClientChannelInitializer(SslContext context, String peerHost, int peerPort)
{
this.context = context;
this.peerHost = peerHost;
this.peerPort = peerPort;
}
#Override
protected void initChannel(Channel channel) throws Exception
{
// Add SSL handler first to encrypt and decrypt everything.
channel.pipeline().addLast(this.context.newHandler(channel.alloc(), this.peerHost, this.peerPort));
// and then business logic.
channel.pipeline().addLast(new EchoClientHandler());
}
}
The echo client:
package info.junius.tutorial.echo.netty.tls;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class EchoClient
{
private final String host;
private final int port;
public EchoClient(String host, int port)
{
super();
this.host = host;
this.port = port;
}
public static void main(String[] args) throws Exception
{
if (args.length != 2)
{
System.err.println("Usage: " + EchoClient.class.getSimpleName() + " <host> <port>");
}
else
{
// Security.addProvider(new BouncyCastleProvider());
String host = args[0];
int port = Integer.parseInt(args[1]);
new EchoClient(host, port).start();
}
}
public void start() throws Exception
{
TlsContextUtil tlsContextUtil = new TlsContextUtil();
ChannelInitializer<Channel> channelInitializer = new ClientChannelInitializer(tlsContextUtil.getClientContext(), this.host, this.port);
EventLoopGroup group = new NioEventLoopGroup();
try
{
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class).handler(channelInitializer);
Channel channel = b.connect(this.host, this.port).sync().channel();
ChannelFuture writeFuture = channel.writeAndFlush("Hello from netty client!\n");
// channel.closeFuture().sync();
writeFuture.sync();
}
finally
{
group.shutdownGracefully().sync();
}
}
}
And a utility class that returns an SslContext:
...
public SslContext getClientContext() throws IOException
{
SslContext sslContext = null;
try
{
// truststore
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX", "SunJSSE");
tmf.init(this.getKeystore(TRUSTSTORE));
// keystore holding client certificate
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX", "SunJSSE");
kmf.init(this.getKeystore(CLIENT_KEYSTORE), KEYSTORE_PW);
SslContextBuilder builder = SslContextBuilder.forClient().keyManager(kmf).trustManager(tmf).ciphers(PFS_CIPHERS);
// build context
sslContext = builder.build();
}
catch (NoSuchAlgorithmException
| NoSuchProviderException
| KeyStoreException
| IllegalStateException
| UnrecoverableKeyException e)
{
throw new IOException("Unable to create client TLS context", e);
}
return sslContext;
}
...
VM arguments:
-Djavax.net.debug=all -Djava.security.debug="certpath crl" -Dcom.sun.net.ssl.checkRevocation=true -Dcom.sun.security.enableCRLDP=true
I'm quite confident that my mistake must be in the Netty client code, because the system works fine when using JSSE only. Any help is highly appreciated!
Cheers,
Andy
OK, I've got it to work. It was actually my client code that was wrong (the code was based on the secure chat example that comes with Netty). So I changed it to the version used in the echo example:
EchoClientHandler:
#Override
public void channelActive(ChannelHandlerContext ctx)
{
// When notified that the channel is active send a message.
System.out.println("CLIENT: Sending request to server...");
ctx.writeAndFlush(Unpooled.copiedBuffer("Mein Schnitzel ist kaputt!\n", CharsetUtil.UTF_8));
}
and the EchoClient:
try
{
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class).handler(channelInitializer);
ChannelFuture f = b.connect(this.host, this.port).sync();
f.channel().closeFuture().sync();
}
finally
{
group.shutdownGracefully().sync();
}
The previous code just disconnected too early, so that the handshake never completed.
I have oAuth 2.0 code that works with other services (such as LinkedIn and Facebook) but niot Google.
The code fails with 'Consumer is not registered'. It certainly is. That is if this error means what I think it means but I do have the following in https://code.google.com/apis/console.
a project,
and a valid CLIENT_ID entry
Client ID: 107***********4ek7fl.apps.googleusercontent.com
Client secret: Q6KbA**********FRbL
Redirect URIs: urn:ietf:wg:oauth:2.0:oob, htp://{localhost}
The failure occurs when the request is first sent to https://accounts.google.com/o/oauth2/auth
No page is displayed asking the user to authenticate, the Google server returns "Consumer is not registered" in the response body.
I had a similar problem. I am using the scribe oauth java library, https://github.com/fernandezpablo85/scribe-java. It's support of Google is only oauth 1.0, so I had to write my own class that extends org.scribe.builder.api.DefaultApi20 instead of org.scribe.builder.api.DefaultApi10a.
basically here you are:
package org.scribe.builder.api;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.scribe.exceptions.OAuthException;
import org.scribe.extractors.AccessTokenExtractor;
import org.scribe.model.OAuthConfig;
import org.scribe.model.OAuthConstants;
import org.scribe.model.OAuthRequest;
import org.scribe.model.Response;
import org.scribe.model.Token;
import org.scribe.model.Verb;
import org.scribe.model.Verifier;
import org.scribe.oauth.OAuth20ServiceImpl;
import org.scribe.oauth.OAuthService;
import org.scribe.utils.OAuthEncoder;
import org.scribe.utils.Preconditions;
/**
* Google OAuth2.0
* Released under the same license as scribe (MIT License)
* #author houman001
* This code borrows from and modifies changes made by #yincrash
* #author yincrash
*
*/
public class Google2Api extends DefaultApi20 {
private static final String AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=%s&redirect_uri=%s";
private static final String SCOPED_AUTHORIZE_URL = AUTHORIZE_URL + "&scope=%s";
private static final String SUFFIX_OFFLINE = "&" + OAuthConstants.ACCESS_TYPE + "=" + OAuthConstants.ACCESS_TYPE_OFFLINE
+ "&" + OAuthConstants.APPROVAL_PROMPT + "=" + OAuthConstants.APPROVAL_PROMPT_FORCE;
#Override
public String getAccessTokenEndpoint() {
return "https://accounts.google.com/o/oauth2/token";
}
#Override
public AccessTokenExtractor getAccessTokenExtractor() {
return new AccessTokenExtractor() {
public Token extract(String response) {
Preconditions.checkEmptyString(response, "Response body is incorrect. Can't extract a token from an empty string");
Matcher matcher = Pattern.compile("\"access_token\" : \"([^&\"]+)\"").matcher(response);
if (matcher.find())
{
String token = OAuthEncoder.decode(matcher.group(1));
String refreshToken = "";
Matcher refreshMatcher = Pattern.compile("\"refresh_token\" : \"([^&\"]+)\"").matcher(response);
if (refreshMatcher.find())
refreshToken = OAuthEncoder.decode(refreshMatcher.group(1));
Date expiry = null;
Matcher expiryMatcher = Pattern.compile("\"expires_in\" : ([^,&\"]+)").matcher(response);
if (expiryMatcher.find())
{
int lifeTime = Integer.parseInt(OAuthEncoder.decode(expiryMatcher.group(1)));
expiry = new Date(System.currentTimeMillis() + lifeTime * 1000);
}
Token result = new Token(token, refreshToken, expiry, response);
return result;
}
else
{
throw new OAuthException("Response body is incorrect. Can't extract a token from this: '" + response + "'", null);
}
}
};
}
#Override
public String getAuthorizationUrl(OAuthConfig config) {
// Append scope if present
if (config.hasScope()) {
String format = config.isOffline() ? SCOPED_AUTHORIZE_URL + SUFFIX_OFFLINE : SCOPED_AUTHORIZE_URL;
return String.format(format, config.getApiKey(),
OAuthEncoder.encode(config.getCallback()),
OAuthEncoder.encode(config.getScope()));
} else {
String format = config.isOffline() ? AUTHORIZE_URL + SUFFIX_OFFLINE : AUTHORIZE_URL;
return String.format(format, config.getApiKey(),
OAuthEncoder.encode(config.getCallback()));
}
}
#Override
public Verb getAccessTokenVerb() {
return Verb.POST;
}
#Override
public OAuthService createService(OAuthConfig config) {
return new GoogleOAuth2Service(this, config);
}
private static class GoogleOAuth2Service extends OAuth20ServiceImpl {
private DefaultApi20 api;
private OAuthConfig config;
public GoogleOAuth2Service(DefaultApi20 api, OAuthConfig config) {
super(api, config);
this.api = api;
this.config = config;
}
#Override
public Token getAccessToken(Token requestToken, Verifier verifier) {
OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint());
switch (api.getAccessTokenVerb()) {
case POST:
request.addBodyParameter(OAuthConstants.CLIENT_ID, config.getApiKey());
// API Secret is optional
if (config.getApiSecret() != null && config.getApiSecret().length() > 0)
request.addBodyParameter(OAuthConstants.CLIENT_SECRET, config.getApiSecret());
if (requestToken == null) {
request.addBodyParameter(OAuthConstants.CODE, verifier.getValue());
request.addBodyParameter(OAuthConstants.REDIRECT_URI, config.getCallback());
request.addBodyParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.GRANT_TYPE_AUTHORIZATION_CODE);
} else {
request.addBodyParameter(OAuthConstants.REFRESH_TOKEN, requestToken.getSecret());
request.addBodyParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.GRANT_TYPE_REFRESH_TOKEN);
}
break;
case GET:
default:
request.addQuerystringParameter(OAuthConstants.CLIENT_ID, config.getApiKey());
// API Secret is optional
if (config.getApiSecret() != null && config.getApiSecret().length() > 0)
request.addQuerystringParameter(OAuthConstants.CLIENT_SECRET, config.getApiSecret());
request.addQuerystringParameter(OAuthConstants.CODE, verifier.getValue());
request.addQuerystringParameter(OAuthConstants.REDIRECT_URI, config.getCallback());
if(config.hasScope()) request.addQuerystringParameter(OAuthConstants.SCOPE, config.getScope());
}
Response response = request.send();
return api.getAccessTokenExtractor().extract(response.getBody());
}
}
}
from https://raw.githubusercontent.com/codolutions/scribe-java/master/src/main/java/org/scribe/builder/api/Google2Api.java
I would like to implement push notifications for my website (obviously only in compatible browser as Safari 7).
I have read the Apple documentation and I have successfully created my package containing my icon.iconset, my certificate.p12, manifest.json and a website.json.
Now I would like to ask the permission to the user when I first visit the website. If he allows it, I should send the package.
Everything is pretty clear but I don't know how to go on.
How do I create my push package out of my files? How do I precisely sign it? The package should be always the same so I could sign it on my mac and upload to my server only one package.
If you have experience with this technology, please let me know :)
I have successfully create push package to ask permission on safari web push notifications using REST API in java. Also I have follow steps which provide by Apple officials on there sites.
Please follow below steps to create push package.
Create your web push notification P12 certificate from your apple account.
Create your REST API using https for pushPackage which contain icon.iconset, your certificate.p12, manifest.json and a website.json. p12 certificate must be web push notification certificate.
How to create push package:- Please refer below java code. I have create push package using java servlet. which provide 2 web service end points.
v1/pushpackage/webpushID
v1/log
Servlet which process your push package request which send by safari push notification method
SafariPushPakageAPI.java
/*
Push Package REST API handler
*/
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import com.safari.Packager;
public class SafariPushPakageAPI extends HttpServlet{
/**
*
*/
private static final long serialVersionUID = 1L;
public static String ServerPath = null;
private static final String REQUEST_PERMISSION = "/v1/pushPackages/YOUR_WEB_PUSH_ID";
private static final String REQUEST_ERRORLOG = "/v1/log";
#Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doRequest(request, response);
}
// /v1/pushPackages/webpushID
// /v1/log
#Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doRequest(request, response);
}
private void doRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("===>> SAFARI PUSH NOTIFICATION REQUEST");
String path = request.getPathInfo();
System.out.println("PATH ===>> "+path);
if(path == null){
doRequestPermission(request, response);
}else if (path.equalsIgnoreCase(REQUEST_PERMISSION)){
doRequestPermission(request, response);
}else if (path.equalsIgnoreCase(REQUEST_ERRORLOG)){
doRequestShowErrorLog(request, response);
}else{
doRequestPermission(request, response);
}
}
private void doRequestPermission(HttpServletRequest request,HttpServletResponse response) {
try{
System.out.println("INSIDE REQUEST PERMISSION ==>>>");
System.out.println(IOUtils.toString(request.getReader()));
String authToken = StringUtils.isBlank(request.getParameter("token")) ? "UserTokenRT124DFGH" : StringUtils.trimToEmpty(request.getParameter("token"));
System.out.println("=>>>>>>>>>> USER TOKEN =>>>>>>>>>> "+authToken);
#SuppressWarnings("deprecation")
String packagePath =request.getRealPath("pushPackage.raw/icon.iconset/"); // LOCATION WHERE YOUR PUSH PACKAGE FOLDER CONTAIN LOGOS AND website.json file
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment;filename=\"pushpackage.zip\"");
OutputStream out = response.getOutputStream();
out.write(Packager.createPackageFile(authToken,packagePath));
response.flushBuffer();
}catch(IOException ioe){
ioe.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
private void doRequestShowErrorLog(HttpServletRequest request,HttpServletResponse response) {
try{
System.out.println("ERROR LOG STARTED");
System.out.println(IOUtils.toString(request.getReader()));
System.out.println("END");
}catch(Exception e){
e.printStackTrace();
}
}
}
Packager.java
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.io.IOUtils;
import org.json.JSONArray;
import org.json.JSONObject;
/**
*
* #author Ritesh
*/
public class Packager {
final static String CERTIFICATE_PATH="PATH TO YOUR 12 CERTIFICATE";
final static String CERTIFICATE_PASS="PASSWORD";
static String getJSON(String authenticationToken) throws Exception {
JSONObject obj = new JSONObject();
obj.put("websiteName", "WEB SITE NAME");
obj.put("websitePushID", "WEB PUSH ID");
obj.put("allowedDomains", new JSONArray());
obj.getJSONArray("allowedDomains").put("https://TEST.EXAMPLE.net");//LIST OF DOMAINS ALLOW
obj.put("urlFormatString", "https://TEST.EXAMPLE.net/%#");
obj.put("authenticationToken", authenticationToken);
obj.put("webServiceURL", "https://API.EXAMPLE.COM");//callback URL WITHOUT WEB SERVICE ENDPOINT NAME
return obj.toString();
}
public static byte[] createPackageFile(String authenticationToken, String path) throws Exception {
System.out.println("packaging safari file with token: " + authenticationToken);
ZipHandler zip = new ZipHandler();
File dir = new File(path);
for (File file : dir.listFiles()) {
InputStream is = new FileInputStream(file);
byte[] bytes = IOUtils.toByteArray(is);
zip.addFile("icon.iconset", file.getName(),bytes );
}
zip.addFile("", "website.json", getJSON(authenticationToken).getBytes());
byte[] manifest = zip.manifest();
zip.addFile("", "manifest.json", manifest);
zip.addFile("", "signature", sign(manifest));
return zip.getBytes();
}
static byte[] sign(byte bytesToSign[]) throws Exception {
return new PKCS7Signer().sign(CERTIFICATE_PATH,CERTIFICATE_PASS, bytesToSign);
}
/**
* Servlet handler , should listen on the callback URL (as in webServiceURL)
* #param requestPath
* #param req
* #param servletRequest
* #param servletResponse
* #throws Exception
*/
public static void main(String[] args) throws Exception {
Packager.createPackageFile("SafriNotifcation","");
}
}
PKCS7Signer.java which create your signature file.
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.Store;
public final class PKCS7Signer {
static {
try{
Security.addProvider(new BouncyCastleProvider());
}catch(Exception e){
e.printStackTrace();
}
}
private KeyStore getKeystore(String storeLocation, String storePasswd) throws Exception {
if (storeLocation == null) {
System.out.println("Could not find store file (.p12)");
return null;
}
// First load the keystore object by providing the p12 file path
KeyStore clientStore = KeyStore.getInstance("PKCS12");
// replace testPass with the p12 password/pin
clientStore.load(new FileInputStream(storeLocation), storePasswd.toCharArray());
return clientStore;
}
private X509CertificateHolder getCert(KeyStore keystore, String alias) throws Exception {
java.security.cert.Certificate c = keystore.getCertificate(alias);
return new X509CertificateHolder(c.getEncoded());
}
private PrivateKey getPrivateKey(KeyStore keystore, String alias, String storePasswd) throws Exception {
return (PrivateKey) keystore.getKey(alias, storePasswd.toCharArray());
}
public byte[] sign(String storeLocation, String storePasswd, byte[] dataToSign) throws Exception {
KeyStore clientStore = getKeystore(storeLocation, storePasswd);
if (clientStore == null) {
return null;
}
Enumeration aliases = clientStore.aliases();
String alias = "";
while (aliases.hasMoreElements()) {
alias = (String) aliases.nextElement();
if (clientStore.isKeyEntry(alias)) {
break;
}
}
CMSTypedData msg = new CMSProcessableByteArray(dataToSign); // Data to sign
X509CertificateHolder x509Certificate = getCert(clientStore, alias);
List certList = new ArrayList();
certList.add(x509Certificate); // Adding the X509 Certificate
Store certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
// Initializing the the BC's Signer
ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(
getPrivateKey(clientStore, alias, storePasswd));
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder()
.setProvider("BC").build()).build(sha1Signer, x509Certificate));
// adding the certificate
gen.addCertificates(certs);
// Getting the signed data
CMSSignedData sigData = gen.generate(msg, false);
return sigData.getEncoded();
}
}
please note used latest bouncy castle jars for create signature file
bcprov-jdk15on-157.jar
bcpkix-jdk15on-157.jar
Write your javascript on your client side page.it contains simple java script to get permissions.
///
// For safari
var domain="YOUR WEB PUSH ID";
function safariIniti() {
var pResult = window.safari.pushNotification.permission(domain);
if(pResult.permission === 'default') {
//request permission
requestPermissions();
} else if (pResult.permission === 'granted') {
console.log("Permission for " + domain + " is " + pResult.permission);
var token = pResult.deviceToken;
// Show subscription for debug
console.log('Subscription details:'+token);
} else if(pResult.permission === 'denied') {
console.log("Permission for " + domain + " is " + pResult.permission);
}
}
function getToken(){
// always start with a letter (for DOM friendlyness)
var idstr=String.fromCharCode(Math.floor((Math.random()*25)+65));
do {
// between numbers and characters (48 is 0 and 90 is Z (42-48 = 90)
var ascicode=Math.floor((Math.random()*42)+48);
if (ascicode<58 || ascicode>64){
// exclude all chars between : (58) and # (64)
idstr+=String.fromCharCode(ascicode);
}
} while (idstr.length<32);
return (idstr);
}
function requestPermissions() {
var tokenVal = getToken();
window.safari.pushNotification.requestPermission('WEb service url without end points',domain,{token:tokenVal},
function(subscription) {
console.log(subscription.permission);
console.log("PERMISSION ====>> "+subscription.permission);
if(subscription.permission === 'granted') {
//TODO
}
else if(subscription.permission === 'denied') {
// TODO:
}
});
}
Apple provides a php file that you can use to create your push package including the signature. https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NotificationProgrammingGuideForWebsites/CompanionFile.zip
Alternatively, you can use the push_package gem, https://github.com/SymmetricInfinity/push_package, which we developed when implementing safari push notifications for zeropush.com. More details are available at https://zeropush.com/blog/implementing-safari-push-notifications-in-osx-mavericks.
Follow this apple documentation and github repo, they include sufficient information required to create a safari push notifications.