Skip to content

Commit d86af09

Browse files
authored
[feat] add eTag encrypting/decrypting (#554)
* add etag encrypting * fix
1 parent cf52c3a commit d86af09

File tree

5 files changed

+151
-13
lines changed

5 files changed

+151
-13
lines changed

dao-api/src/main/pegasus/pegasus/com/linkedin/metadata/events/IngestionAspectETag.pdl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ record IngestionAspectETag {
88
/**
99
* aspect field name, e.g. "status"
1010
*/
11-
aspect_name: optional string = ""
11+
aspect_alias: optional string = ""
1212

1313
/**
14-
* e.g. timestamp used for optimistic locking when writing new aspect value
14+
* e.g. used for optimistic locking when writing new aspect value
1515
*/
16-
eTag: optional long = 0
16+
etag: optional string = ""
1717
}

dao-impl/ebean-dao/src/main/java/com/linkedin/metadata/dao/EbeanLocalDAO.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.linkedin.metadata.dao.urnpath.EmptyPathExtractor;
2323
import com.linkedin.metadata.dao.urnpath.UrnPathExtractor;
2424
import com.linkedin.metadata.dao.utils.EBeanDAOUtils;
25+
import com.linkedin.metadata.dao.utils.ETagUtils;
2526
import com.linkedin.metadata.dao.utils.ModelUtils;
2627
import com.linkedin.metadata.dao.utils.QueryUtils;
2728
import com.linkedin.metadata.dao.utils.RecordUtils;
@@ -631,16 +632,34 @@ public <ASPECT extends RecordTemplate> AuditStamp extractOptimisticLockForAspect
631632
continue;
632633
}
633634

634-
if (aspectAlias != null && aspectAlias.equalsIgnoreCase(ingestionAspectETag.getAspect_name()) && ingestionAspectETag.getETag() != null) {
635-
optimisticLockAuditStamp = new AuditStamp();
636-
optimisticLockAuditStamp.setTime(ingestionAspectETag.getETag());
637-
break;
635+
if (aspectAlias != null && aspectAlias.equalsIgnoreCase(ingestionAspectETag.getAspect_alias())) {
636+
Long decryptedETag = getDecryptedETag(ingestionAspectETag);
637+
if (decryptedETag != null) {
638+
optimisticLockAuditStamp = new AuditStamp();
639+
optimisticLockAuditStamp.setTime(decryptedETag);
640+
break;
641+
}
638642
}
639643
}
640644
}
641645
return optimisticLockAuditStamp;
642646
}
643647

648+
/**
649+
* When eTag is null, it means this is a regular ingestion request, no read-modify-write consistency guarantee.
650+
*/
651+
@Nullable
652+
private Long getDecryptedETag(@Nonnull IngestionAspectETag ingestionAspectETag) {
653+
try {
654+
if (ingestionAspectETag.getEtag() == null) {
655+
return null;
656+
}
657+
return ETagUtils.decrypt(ingestionAspectETag.getEtag());
658+
} catch (Exception e) {
659+
return null;
660+
}
661+
}
662+
644663
@Override
645664
protected <ASPECT extends RecordTemplate> long saveLatest(@Nonnull URN urn, @Nonnull Class<ASPECT> aspectClass,
646665
@Nullable ASPECT oldValue, @Nullable AuditStamp optimisticLockAuditStamp, @Nullable ASPECT newValue,
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.linkedin.metadata.dao.utils;
2+
3+
import java.security.InvalidKeyException;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.util.Base64;
6+
import javax.annotation.Nonnull;
7+
import javax.crypto.BadPaddingException;
8+
import javax.crypto.Cipher;
9+
import javax.crypto.IllegalBlockSizeException;
10+
import javax.crypto.NoSuchPaddingException;
11+
import javax.crypto.SecretKey;
12+
import javax.crypto.spec.SecretKeySpec;
13+
14+
15+
/**
16+
* Utility class for IngestionAspectETag. E.g., encrypting and decrypting timestamps using AES encryption.
17+
*/
18+
public final class ETagUtils {
19+
20+
public static final String AES = "AES";
21+
22+
private ETagUtils() {
23+
}
24+
25+
// 16-char key for AES-128
26+
private static final String SECRET_KEY = "9012312344567856";
27+
28+
/**
29+
* Encrypts a timestamp using AES encryption.
30+
* @param timestamp Timestamp to encrypt
31+
* @return Encrypted timestamp as a Base64 encoded string
32+
*/
33+
@Nonnull
34+
public static String encrypt(long timestamp)
35+
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException,
36+
BadPaddingException {
37+
SecretKey secretKey = getSecretKey();
38+
39+
Cipher cipher = Cipher.getInstance(AES);
40+
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
41+
42+
byte[] inputBytes = Long.toString(timestamp).getBytes();
43+
byte[] encrypted = cipher.doFinal(inputBytes);
44+
45+
return Base64.getEncoder().encodeToString(encrypted);
46+
}
47+
48+
/**
49+
* Decrypts a Base64 encoded encrypted timestamp string back to the original timestamp.
50+
* @param encrypted Base64 encoded encrypted timestamp string
51+
* @return Decrypted timestamp as a long
52+
*/
53+
public static long decrypt(@Nonnull String encrypted)
54+
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException,
55+
BadPaddingException {
56+
SecretKey secretKey = getSecretKey();
57+
58+
Cipher cipher = Cipher.getInstance(AES);
59+
cipher.init(Cipher.DECRYPT_MODE, secretKey);
60+
61+
byte[] decoded = Base64.getDecoder().decode(encrypted);
62+
byte[] decrypted = cipher.doFinal(decoded);
63+
64+
return Long.parseLong(new String(decrypted));
65+
}
66+
67+
private static SecretKey getSecretKey() {
68+
byte[] keyBytes = SECRET_KEY.getBytes();
69+
return new SecretKeySpec(keyBytes, AES);
70+
}
71+
72+
}

dao-impl/ebean-dao/src/test/java/com/linkedin/metadata/dao/EbeanLocalDAOTest.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4064,22 +4064,24 @@ private void assertVersionMetadata(ListResultMetadata listResultMetadata, List<L
40644064
}
40654065

40664066
@Test
4067-
public void testExtractOptimisticLockForAspectFromIngestionParamsIfPossible() throws URISyntaxException {
4067+
public void testExtractOptimisticLockForAspectFromIngestionParamsIfPossible()
4068+
throws URISyntaxException {
40684069
EbeanLocalDAO<EntityAspectUnion, FooUrn> dao = createDao(FooUrn.class);
40694070

40704071
FooUrn urn = new FooUrn(1);
40714072

40724073
IngestionAspectETag ingestionAspectETag = new IngestionAspectETag();
4073-
ingestionAspectETag.setAspect_name("aspectFoo");
4074-
ingestionAspectETag.setETag(1234L);
4074+
ingestionAspectETag.setAspect_alias("aspectFoo");
4075+
long timestamp = 1750796203701L;
4076+
ingestionAspectETag.setEtag("KsFkRXtjaBGQf37HjdEjDQ==");
40754077

40764078
IngestionParams ingestionParams = new IngestionParams();
40774079
ingestionParams.setIngestionETags(new IngestionAspectETagArray(ingestionAspectETag));
40784080

40794081
AuditStamp result = dao.extractOptimisticLockForAspectFromIngestionParamsIfPossible(ingestionParams, AspectFoo.class,
40804082
urn);
40814083

4082-
assertEquals(result.getTime(), Long.valueOf(1234L));
4084+
assertEquals(result.getTime(), Long.valueOf(timestamp));
40834085
}
40844086

40854087
@Test
@@ -4103,8 +4105,8 @@ public void testExtractOptimisticLockForAspectFromIngestionParamsIfPossibleAspec
41034105
FooUrn urn = new FooUrn(1);
41044106

41054107
IngestionAspectETag ingestionAspectETag = new IngestionAspectETag();
4106-
ingestionAspectETag.setAspect_name("aspectBar");
4107-
ingestionAspectETag.setETag(1234L);
4108+
ingestionAspectETag.setAspect_alias("aspectBar");
4109+
ingestionAspectETag.setEtag("KsFkRXtjaBGQf37HjdEjDQ==");
41084110

41094111
IngestionParams ingestionParams = new IngestionParams();
41104112
ingestionParams.setIngestionETags(new IngestionAspectETagArray(ingestionAspectETag));
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.linkedin.metadata.dao.utils;
2+
3+
import java.security.InvalidKeyException;
4+
import java.security.NoSuchAlgorithmException;
5+
import javax.crypto.BadPaddingException;
6+
import javax.crypto.IllegalBlockSizeException;
7+
import javax.crypto.NoSuchPaddingException;
8+
import org.testng.annotations.Test;
9+
10+
import static org.junit.Assert.*;
11+
12+
13+
public class ETagUtilsTest {
14+
15+
@Test
16+
public void testEncryptAndDecrypt()
17+
throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException,
18+
InvalidKeyException {
19+
long timestamp = System.currentTimeMillis();
20+
String encrypted = ETagUtils.encrypt(timestamp);
21+
22+
long decrypted = ETagUtils.decrypt(encrypted);
23+
assertEquals(decrypted, timestamp);
24+
}
25+
26+
@Test
27+
public void testEncrypt()
28+
throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException,
29+
InvalidKeyException {
30+
long timestamp = 1750796203701L;
31+
String encrypted = ETagUtils.encrypt(timestamp);
32+
33+
assertEquals("KsFkRXtjaBGQf37HjdEjDQ==", encrypted);
34+
}
35+
36+
@Test
37+
public void testDecrypt()
38+
throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException,
39+
InvalidKeyException {
40+
String encrypted = "KsFkRXtjaBGQf37HjdEjDQ==";
41+
long decrypted = ETagUtils.decrypt(encrypted);
42+
43+
assertEquals(1750796203701L, decrypted);
44+
}
45+
}

0 commit comments

Comments
 (0)