Hi @Shubham, @bhaskar.dabhi,
Please find the below java classes which has been used for encryption and has been working with PHR app successfully. Please have a look and let us know if it helps.
DataEntryFactory class
Method process
taken list of entries and key material and returns list of encrypted entries.
@AllArgsConstructor
public class DataEntryFactory {
public static final String HEALTH_INFORMATION_LINK = "%s/health-information/%s?token=%s";
public static final String MEDIA_APPLICATION_FHIR_JSON = "application/fhir+json";
private final Encryptor encryptor;
public Mono<EncryptedEntries> process(Entries entries, KeyMaterial dataRequestKeyMaterial)
throws Exception {
var keyPair = Encryptor.generateKeyPair();
var randomKey = Encryptor.generateRandomKey();
List<Entry> processedEntries = new ArrayList<>();
entries.getCareBundles().forEach(careBundle -> {
String encryptData = "";
try {
encryptData = encryptor.encryptData(dataRequestKeyMaterial, keyPair, careBundle.getContent(), randomKey);
} catch (Exception exception) {
exception.printStackTrace();
}
if (StringUtils.isNotEmpty(encryptData)) {
processedEntries.add(componentEntry(encryptData, careBundle.getCareContextReference()));
}
});
var keyStructure = new KeyStructure(LocalDateTime.now(ZoneOffset.UTC),
dataRequestKeyMaterial.getDhPublicKey().getParameters(),
Encryptor.getBase64String(Encryptor.getEncodedPublicKeyForProjectEKAHIU(keyPair.getPublic())));
var keyMaterial = new KeyMaterial(dataRequestKeyMaterial.getCryptoAlg(),
dataRequestKeyMaterial.getCurve(), keyStructure, randomKey);
return Mono.just(new EncryptedEntries(processedEntries, keyMaterial));
}
private static Entry entryWith(String content, String link, String careContextReference)
{
return Entry.builder()
.content(content)
.media(MEDIA_APPLICATION_FHIR_JSON)
.checksum("MD5")
.link(link)
.careContextReference(careContextReference)
.build();
}
private static Entry componentEntry(String serializedBundle, String careContextReference)
{
return entryWith(serializedBundle, null, careContextReference);
}
}
Encryptor.java
Method encryptData
called by above class which does the actual enncryption.
public class Encryptor {
private static final Logger logger = LoggerFactory.getLogger(Encryptor.class);
public static final String ALGORITHM = "ECDH";
public static final String CURVE = "curve25519";
public static final String PROVIDER = BouncyCastleProvider.PROVIDER_NAME;
public String encryptData(KeyMaterial keyMaterial,
KeyPair keyPair,
String content, String randomKey) throws Exception
{
String privateKey = getBase64String(Encryptor.getEncodedPrivateKey(keyPair.getPrivate()));
var nonce = keyMaterial.getNonce();
byte[] xorOfRandom = xorOfRandom(randomKey, nonce);
return encrypt(xorOfRandom, privateKey, keyMaterial.getDhPublicKey().getKeyValue(), content);
}
private static String encrypt(byte[] xorOfRandom, String senderPrivateKey, String receiverPublicKey, String stringToEncrypt) throws Exception {
String sharedKey = doECDH(getBytesForBase64String(senderPrivateKey), getBytesForBase64String(receiverPublicKey));
byte[] iv = Arrays.copyOfRange(xorOfRandom, xorOfRandom.length - 12, xorOfRandom.length);
byte[] aesKey = generateAesKey(xorOfRandom, sharedKey);
String encryptedData = "";
try {
byte[] stringBytes = stringToEncrypt.getBytes();
GCMBlockCipher cipher = new GCMBlockCipher(new AESEngine());
AEADParameters parameters =
new AEADParameters(new KeyParameter(aesKey), 128, iv, null);
cipher.init(true, parameters);
byte[] plainBytes = new byte[cipher.getOutputSize(stringBytes.length)];
int retLen = cipher.processBytes
(stringBytes, 0, stringBytes.length, plainBytes, 0);
cipher.doFinal(plainBytes, retLen);
encryptedData = getBase64String(plainBytes);
} catch (Exception exception) {
logger.error(exception.getMessage(), exception);
}
return encryptedData;
}
public static String generateRandomKey() {
byte[] salt = new byte[32];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
return getBase64String(salt);
}
public static KeyPair generateKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
Security.addProvider(new BouncyCastleProvider());
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM, PROVIDER);
X9ECParameters ecParameters = CustomNamedCurves.getByName(CURVE);
ECParameterSpec ecSpec=new ECParameterSpec(ecParameters.getCurve(), ecParameters.getG(),
ecParameters.getN(), ecParameters.getH(), ecParameters.getSeed());
keyPairGenerator.initialize(ecSpec, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
private static PrivateKey loadPrivateKey (byte [] data) throws Exception
{
X9ECParameters ecP = CustomNamedCurves.getByName(CURVE);
ECParameterSpec params=new ECParameterSpec(ecP.getCurve(), ecP.getG(),
ecP.getN(), ecP.getH(), ecP.getSeed());
ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(data), params);
KeyFactory kf = KeyFactory.getInstance(ALGORITHM, PROVIDER);
return kf.generatePrivate(privateKeySpec);
}
private static PublicKey loadPublicKey (byte [] data) throws Exception
{
Security.addProvider(new BouncyCastleProvider());
X9ECParameters ecP = CustomNamedCurves.getByName(CURVE);
ECParameterSpec ecNamedCurveParameterSpec = new ECParameterSpec(ecP.getCurve(), ecP.getG(),
ecP.getN(), ecP.getH(), ecP.getSeed());
return KeyFactory.getInstance(ALGORITHM, PROVIDER)
.generatePublic(new ECPublicKeySpec(ecNamedCurveParameterSpec.getCurve().decodePoint(data),
ecNamedCurveParameterSpec));
}
private static String doECDH (byte[] dataPrv, byte[] dataPub) throws Exception
{
KeyAgreement ka = KeyAgreement.getInstance(ALGORITHM, PROVIDER);
ka.init(loadPrivateKey(dataPrv));
ka.doPhase(loadPublicKey(dataPub), true);
byte [] secret = ka.generateSecret();
return getBase64String(secret);
}
private static byte [] xorOfRandom(String randomKeySender, String randomKeyReceiver)
{
byte[] randomSender = getBytesForBase64String(randomKeySender);
byte[] randomReceiver = getBytesForBase64String(randomKeyReceiver);
byte[] out = new byte[randomSender.length];
for (int i = 0; i < randomSender.length; i++) {
out[i] = (byte) (randomSender[i] ^ randomReceiver[i%randomReceiver.length]);
}
return out;
}
private static byte [] generateAesKey(byte[] xorOfRandoms, String sharedKey ){
byte[] salt = Arrays.copyOfRange(xorOfRandoms, 0, 20);
HKDFBytesGenerator hkdfBytesGenerator = new HKDFBytesGenerator(new SHA256Digest());
HKDFParameters hkdfParameters = new HKDFParameters(getBytesForBase64String(sharedKey), salt, null);
hkdfBytesGenerator.init(hkdfParameters);
byte[] aesKey = new byte[32];
hkdfBytesGenerator.generateBytes(aesKey, 0, 32);
return aesKey;
}
public static String getBase64String(byte[] value){
return new String(org.bouncycastle.util.encoders.Base64.encode(value));
}
public static byte[] getBytesForBase64String(String value){
return org.bouncycastle.util.encoders.Base64.decode(value);
}
public static byte[] getEncodedPublicKeyForProjectEKAHIU(PublicKey key){
ECPublicKey ecKey = (ECPublicKey)key;
return ecKey.getEncoded();
}
public static byte [] getEncodedPrivateKey(PrivateKey key) throws Exception
{
ECPrivateKey ecKey = (ECPrivateKey)key;
return ecKey.getD().toByteArray();
}
}
Def for EncryptedEntries
and Entry
. Annotations are from Lombok.
@Value
@Builder
@AllArgsConstructor
public class EncryptedEntries {
List<Entry> entries;
KeyMaterial keyMaterial;
@Value
@Builder
public static class Entry {
String content;
String media;
String checksum;
String link;
String careContextReference;
}
}
Def for KeyMaterial
and KeyStructure
. Annotations are from Lombok.
@Value
@Builder
@AllArgsConstructor
public class KeyMaterial {
String cryptoAlg;
String curve;
KeyStructure dhPublicKey;
String nonce;
@Value
@Builder
@AllArgsConstructor
public static class KeyStructure {
LocalDateTime expiry;
String parameters;
String keyValue;
}
}
Def for Entries
. Annotations are from Lombok.
@Value
@Builder
public class Entries {
List<CareBundle> careBundles;
@Value
@Builder
public class CareBundle {
String careContextReference;
String content;
}
}
You can ignore the Mono part and just return the EncryptedEntries
. This is present because we are using reactive java stack. In the last line replace
return Mono.just(new EncryptedEntries(processedEntries, keyMaterial))
With
return new EncryptedEntries(processedEntries, keyMaterial)