4

I have implemented Secure Web hook features for my Spring Boot application(Java).

For that I have created "Subscription" with below JSON.

String subscriptionMessageTemplate = "{\"changeType\": \"created,updated\",\"notificationUrl\": \"%s/notify/messages\",\"lifecycleNotificationUrl\":\"%s/notify/messages/lifeCycle\", \"resource\": \"/teams/{id}/channels/19:{id}@thread.skype/messages\", \"clientState\": \"secretClientValue\",\"includeResourceData\": true,\"encryptionCertificate\": \"%s\",\"expirationDateTime\":\"%s\",\"encryptionCertificateId\": \"1\"}";

I have used ngrok for public IP: enter image description here

When I am sending message from the team, I am getting below response.

{
    "value": [
        {
            "subscriptionId": "76222963-cc7b-42d2-882d-8aaa69cb2ba3",
            "changeType": "created",
            // Other properties typical in a resource change notification
            "resource": "teams('d29828b8-c04d-4e2a-b2f6-07da6982f0f0')/channels('19:f127a8c55ad949d1a238464d22f0f99e@thread.skype')/messages('1565045424600')/replies('1565047490246')",
            "resourceData": {
                "id": "1565293727947",
                "@odata.type": "#Microsoft.Graph.ChatMessage",
                "@odata.id": "teams('88cbc8fc-164b-44f0-b6a6-b59b4a1559d3')/channels('19:8d9da062ec7647d4bb1976126e788b47@thread.tacv2')/messages('1565293727947')/replies('1565293727947')"
            },
            "encryptedContent": {
                "data": "{encrypted data that produces a full resource}",
        "dataSignature": "<HMAC-SHA256 hash>",
                "dataKey": "{encrypted symmetric key from Microsoft Graph}",
                "encryptionCertificateId": "MySelfSignedCert/DDC9651A-D7BC-4D74-86BC-A8923584B0AB",
                "encryptionCertificateThumbprint": "07293748CC064953A3052FB978C735FB89E61C3D"
            }
        }
    ],
    "validationTokens": [
        "eyJ0eXAiOiJKV1QiLCJhbGciOiJSU..."
    ]
}

Now I want to decrypt data, Can any one help me to how to decrypt data in Java? For certificate generation, I have used my custom method: strong text.

 private void generateSelfSignedX509Certificate(KeyPair keyPair) 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 X509 CERTIFICATE
    X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
    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");

    this.certificate = certGen.generate(keyPair.getPrivate(), "BC");
}
  • This error has been resolved now: byte[] dataKey = Base64.getDecoder().decode([dataKey].getBytes()); final Cipher cipher1 = Cipher.getInstance("RSA/None/OAEPWithSHA1AndMGF1Padding"); cipher1.init(Cipher.DECRYPT_MODE, RSAKeyGenService.getStoredPrivateKey()); byte[] plaintext = cipher1.doFinal(dataKey); Now used that plainText in to decrypt the key as below: – user3459513 Mar 13 '20 at 05:59
  • try { byte[] newArray = Arrays.copyOfRange(plaintext, 0, 16); IvParameterSpec iv = new IvParameterSpec(newArray); SecretKeySpec skeySpec = new SecretKeySpec(plaintext, "AES"); Cipher cipherd = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipherd.init(Cipher.DECRYPT_MODE, skeySpec, iv); byte[] original = cipherd.doFinal(Base64.getDecoder().decode(jsonSubscriptionResponse.getValue().get(0).getEncryptedContent().getData().getBytes())); LOGGER.info(new String(original)); } catch (Exception ex) { ex.printStackTrace(); } – user3459513 Mar 13 '20 at 06:03

3 Answers3

3

I had to struggle my way with it in Python 3.6, so for the sake of future python-readers, here is my skeleton of working code that do the above (using pycryptodome==3.9.7):

import json
import hashlib, hmac
from base64 import b64decode, b64encode
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Util.Padding import unpad
from Crypto.PublicKey import RSA
from Crypto.Cipher import AES

...
...

encrypted_symmetric_key: bytes = b64decode(encrypted_symmetric_key.encode())
encrypted_payload = b64decode(encrypted_payload.encode())

rsa_key = RSA.import_key(private_key, passphrase=private_key_passphrase)
cipher = PKCS1_OAEP.new(rsa_key)
# if length of encrypted_symmetric_key is > 128 we will get ciphertext with incorrect length, to avoid that lets split and decrypt in chunks
default_length = 128
length = len(encrypted_symmetric_key)
if length < default_length:
    decrypt_byte = cipher.decrypt(encrypted_symmetric_key)
else:
    offset = 0
    res = []
    while length - offset > 0:
        if length - offset > default_length:
            res.append(cipher.decrypt(encrypted_symmetric_key[offset:offset + default_length]))
        else:
            res.append(cipher.decrypt(encrypted_symmetric_key[offset:]))
        offset += default_length
    decrypt_byte = b''.join(res)
decrypted_symmetric_key = decrypt_byte

hash_state_machine = hmac.new(decrypted_symmetric_key, msg=encrypted_payload, digestmod=hashlib.sha256)
raw_signature = hash_state_machine.digest()

actual_signature_bytes: bytes = b64encode(raw_signature)
actual_signature: str = actual_signature_bytes.decode()

if actual_signature != expected_data_signature:
   raise Exception("data hash is not as expected")

iv = decrypted_symmetric_key[:16]
cipher2 = AES.new(decrypted_symmetric_key, AES.MODE_CBC, iv=iv)

message_str = unpad(cipher2.decrypt(encrypted_payload), block_size=16).decode()
message_dict = json.loads(message_str)
aikadakh
  • 57
  • 7
Noam
  • 1,640
  • 4
  • 26
  • 55
  • Using `iv = decrypted_symmetric_key[:16]` instead of `iv = os.urandom(16)` was critical for me when using `cryptography.hazmat.primitives.ciphers` – Jorge Jan 29 '23 at 14:54
1

I updated the documentation to include Java samples. I'll also include the samples here for reference but future readers should refer to the documentation which is where the samples will be kept up to date.
Keep in mind these samples operate under the assumption that you have a local Java Key Store (JKS) that the certificate is pulled from.

Decrypt the AES key:

String storename = ""; //name/path of the jks store
String storepass = ""; //password used to open the jks store
String alias = ""; //alias of the certificate when store in the jks store, should be passed as encryptionCertificateId when subscribing and retrieved from the notification
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(storename), storepass.toCharArray());
Key asymmetricKey = ks.getKey(alias, storepass.toCharArray());
byte[] encryptedSymetricKey = Base64.decodeBase64("<value from dataKey property>");
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, asymmetricKey);
byte[] decryptedSymmetricKey = cipher.doFinal(encryptedSymetricKey);

Verify the data signature

byte[] decryptedSymmetricKey = "<the aes key decrypted in the previous step>";
byte[] decodedEncryptedData = Base64.decodeBase64("data property from encryptedContent object");
Mac mac = Mac.getInstance("HMACSHA256");
SecretKey skey = new SecretKeySpec(decryptedSymmetricKey, "HMACSHA256");
mac.init(skey);
byte[] hashedData = mac.doFinal(decodedEncryptedData);
String encodedHashedData = new String(Base64.encodeBase64(hashedData));
if (comparisonSignature.equals(encodedHashedData);)
{
    // Continue with decryption of the encryptedPayload.
}
else
{
    // Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
}

Decrypt the data content

SecretKey skey = new SecretKeySpec(decryptedSymmetricKey, "AES");
IvParameterSpec ivspec = new IvParameterSpec(Arrays.copyOf(decryptedSymmetricKey, 16));
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, skey, ivspec);
String decryptedResourceData = new String(cipher.doFinal(Base64.decodeBase64(encryptedData)));
baywet
  • 4,377
  • 4
  • 20
  • 49
0

optimized the previous answer a bit excluding the hash validations

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from config.config import Config
import base64
import json

config = Config('config.yaml')
notification_response = json.loads(msg)
base64encodedKey = notification_response['value'][0]['encryptedContent']['dataKey']

asymetricPrivateKey = bytes(config.subscription.PRIVATE_KEY,encoding='utf-8')
decodedKey = base64.b64decode(base64encodedKey)
private_key = serialization.load_pem_private_key(
            asymetricPrivateKey,
            password=None,
            backend=default_backend()
        )
decryptedSymetricKey = private_key.decrypt(
        decodedKey,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA1()),
            algorithm=hashes.SHA1(),
            label=None
        )
    )
encrypted_payload = base64.b64decode(notification_response['value'][0]['encryptedContent']['data'].encode()) 
iv = decryptedSymetricKey[:16]
cipher2 = AES.new(decryptedSymetricKey, AES.MODE_CBC, iv=iv)
message_str = unpad(cipher2.decrypt(encrypted_payload), block_size=16).decode()
message_dict = json.loads(message_str)
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 19 '22 at 06:57
  • @Pasindu Madushan where is the config.subscription.PRIVATE_KEY coming from? Could you please tell me what value of this? or show me the .yaml file? – Shahriar Rahman Zahin Dec 01 '22 at 08:49