Explorar o código

encrypt portfolios

Daniel Bohry hai 8 meses
pai
achega
c6fdd51629

+ 18 - 2
src/main/java/com/danielbohry/stocks/domain/Portfolio.java

@@ -1,5 +1,6 @@
 package com.danielbohry.stocks.domain;
 
+import com.danielbohry.stocks.repository.PortfolioEntity;
 import lombok.Builder;
 import lombok.Data;
 
@@ -21,12 +22,27 @@ public class Portfolio {
 
     public BigDecimal getTotalValue() {
         return this.stocks.stream()
-                .map(Stock::getTotal)
-                .reduce(BigDecimal.ZERO, BigDecimal::add);
+            .map(Stock::getTotal)
+            .reduce(BigDecimal.ZERO, BigDecimal::add);
     }
 
     public Integer getTotalAssets() {
         return this.stocks.size();
     }
 
+    public static Portfolio convert(PortfolioEntity entity) {
+        if (entity == null) return null;
+
+        return Portfolio.builder()
+            .id(entity.getId())
+            .stocks(entity.getStocks().stream()
+                .map(stock -> new Stock(stock.getCode(), null, stock.getQuantity(), BigDecimal.ZERO, BigDecimal.ZERO))
+                .toList())
+            .username(entity.getUsername())
+            .createdAt(entity.getCreatedAt())
+            .updatedAt(entity.getUpdatedAt())
+
+            .build();
+    }
+
 }

+ 35 - 0
src/main/java/com/danielbohry/stocks/repository/PortfolioEntity.java

@@ -0,0 +1,35 @@
+package com.danielbohry.stocks.repository;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Document("portfolio")
+public class PortfolioEntity {
+
+    private String id;
+    private String encryptedStocks;
+    private String username;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+    private List<PortfolioStock> stocks = new ArrayList<>();
+
+    @Data
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class PortfolioStock {
+        private String code;
+        private int quantity;
+    }
+
+}

+ 3 - 4
src/main/java/com/danielbohry/stocks/repository/PortfolioRepository.java

@@ -1,6 +1,5 @@
 package com.danielbohry.stocks.repository;
 
-import com.danielbohry.stocks.domain.Portfolio;
 import org.springframework.data.mongodb.repository.Aggregation;
 import org.springframework.data.mongodb.repository.MongoRepository;
 import org.springframework.data.mongodb.repository.Query;
@@ -9,12 +8,12 @@ import org.springframework.stereotype.Repository;
 import java.util.List;
 
 @Repository
-public interface PortfolioRepository extends MongoRepository<Portfolio, String> {
+public interface PortfolioRepository extends MongoRepository<PortfolioEntity, String> {
 
     @Query("{ 'stocks': { $size: 0 } }")
-    List<Portfolio> findAllByEmptyStocks();
+    List<PortfolioEntity> findAllByEmptyStocks();
 
-    List<Portfolio> findAllByUsername(String username);
+    List<PortfolioEntity> findAllByUsername(String username);
 
     @Aggregation(pipeline = {
         "{ '$project': { '_id': 1 } }"

+ 99 - 0
src/main/java/com/danielbohry/stocks/service/PortfolioEncryptService.java

@@ -0,0 +1,99 @@
+package com.danielbohry.stocks.service;
+
+import com.danielbohry.stocks.repository.PortfolioEntity;
+import com.danielbohry.stocks.repository.PortfolioEntity.PortfolioStock;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.List;
+
+@Service
+public class PortfolioEncryptService {
+
+    @Value("${encryption.key}")
+    private String secret;
+
+    private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
+    private static final int IV_LENGTH = 16;
+    private static final int AES_KEY_SIZE = 16;
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    public String encryptStocks(List<PortfolioStock> stocks) {
+        try {
+            String toEncrypt = MAPPER.writeValueAsString(stocks);
+            return encrypt(toEncrypt);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to encrypt stocks", e);
+        }
+    }
+
+    public List<PortfolioStock> decryptStocks(String encrypted) {
+        try {
+            String data = decrypt(encrypted);
+            return MAPPER.readValue(data, new TypeReference<>() {
+            });
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to decrypt stocks", e);
+        }
+    }
+
+    public String encrypt(String data) throws Exception {
+        SecretKeySpec key = getSecretKey();
+
+        byte[] iv = new byte[IV_LENGTH];
+        new SecureRandom().nextBytes(iv);
+        IvParameterSpec ivSpec = new IvParameterSpec(iv);
+
+        Cipher cipher = Cipher.getInstance(ALGORITHM);
+        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
+
+        byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
+        byte[] combined = new byte[IV_LENGTH + encrypted.length];
+        System.arraycopy(iv, 0, combined, 0, IV_LENGTH);
+        System.arraycopy(encrypted, 0, combined, IV_LENGTH, encrypted.length);
+
+        return Base64.getEncoder().encodeToString(combined);
+    }
+
+    public String decrypt(String encryptedData) throws Exception {
+        SecretKeySpec key = getSecretKey();
+
+        byte[] combined = Base64.getDecoder().decode(encryptedData);
+        byte[] iv = new byte[IV_LENGTH];
+        byte[] encrypted = new byte[combined.length - IV_LENGTH];
+
+        System.arraycopy(combined, 0, iv, 0, IV_LENGTH);
+        System.arraycopy(combined, IV_LENGTH, encrypted, 0, encrypted.length);
+
+        Cipher cipher = Cipher.getInstance(ALGORITHM);
+        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
+
+        byte[] decrypted = cipher.doFinal(encrypted);
+        return new String(decrypted, StandardCharsets.UTF_8);
+    }
+
+    public boolean isEncrypted(PortfolioEntity entity) {
+        return entity != null &&
+            entity.getEncryptedStocks() != null &&
+            !entity.getEncryptedStocks().isEmpty() &&
+            (entity.getStocks() == null || entity.getStocks().isEmpty());
+    }
+
+    private SecretKeySpec getSecretKey() {
+        byte[] keyBytes = new byte[AES_KEY_SIZE];
+        byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8);
+
+        int length = Math.min(secretBytes.length, AES_KEY_SIZE);
+        System.arraycopy(secretBytes, 0, keyBytes, 0, length);
+
+        return new SecretKeySpec(keyBytes, "AES");
+    }
+}

+ 31 - 17
src/main/java/com/danielbohry/stocks/service/PortfolioService.java

@@ -6,6 +6,8 @@ import com.danielbohry.stocks.domain.Quote;
 import com.danielbohry.stocks.domain.Stock;
 import com.danielbohry.stocks.exception.BadRequestException;
 import com.danielbohry.stocks.exception.NotFoundException;
+import com.danielbohry.stocks.repository.PortfolioEntity;
+import com.danielbohry.stocks.repository.PortfolioEntity.PortfolioStock;
 import com.danielbohry.stocks.repository.PortfolioRepository;
 import com.danielbohry.stocks.service.ExchangeService.ExchangeRateResponse;
 import lombok.AllArgsConstructor;
@@ -16,9 +18,9 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.UUID;
 
+import static com.danielbohry.stocks.domain.Portfolio.convert;
 import static java.time.LocalDateTime.now;
 import static java.util.Collections.emptyList;
 
@@ -30,9 +32,12 @@ public class PortfolioService {
     private final PortfolioRepository repository;
     private final StockService stockService;
     private final ExchangeService exchangeService;
+    private final PortfolioEncryptService portfolioEncryptService;
 
     public List<Portfolio> getAll() {
-        return repository.findAll();
+        return repository.findAll().stream()
+            .map(Portfolio::convert)
+            .toList();
     }
 
     public List<String> getAllIds() {
@@ -40,17 +45,22 @@ public class PortfolioService {
     }
 
     public List<Portfolio> getByUser(String username, String currency) {
-        List<Portfolio> portfolios = repository.findAllByUsername(username);
-
-        return portfolios.stream()
-            .map(portfolio -> get(portfolio.getId(), currency))
+        return repository.findAllByUsername(username).stream()
+            .map(entity -> get(entity.getId(), currency))
             .toList();
     }
 
     public Portfolio get(String id, String currency) {
-        Portfolio portfolio = repository.findById(id)
+        PortfolioEntity entity = repository.findById(id)
             .orElseThrow(() -> new NotFoundException("No portfolio found with id: " + id));
 
+        String encrypted = entity.getEncryptedStocks();
+
+        if (portfolioEncryptService.isEncrypted(entity)) {
+            entity.setStocks(portfolioEncryptService.decryptStocks(encrypted));
+        }
+
+        Portfolio portfolio = convert(entity);
         ExchangeRateResponse exchangeRate = exchangeService.getCurrentRate(currency);
         Map<String, BigDecimal> rates = exchangeRate.getConversionRates();
         BigDecimal targetRate = rates.getOrDefault(currency, BigDecimal.ONE);
@@ -79,7 +89,7 @@ public class PortfolioService {
     public Portfolio create() {
         String id = UUID.randomUUID().toString();
 
-        Portfolio toSave = Portfolio.builder()
+        PortfolioEntity toSave = PortfolioEntity.builder()
             .id(id)
             .stocks(emptyList())
             .username(UserContextHolder.get().getUsername())
@@ -87,23 +97,27 @@ public class PortfolioService {
             .updatedAt(now())
             .build();
 
-        return repository.save(toSave);
+        return convert(repository.save(toSave));
     }
 
     public Portfolio update(String id, List<Stock> stocks) {
         log.info("Updating portfolio [{}]", id);
-        Optional<Portfolio> portfolio = repository.findById(id);
-
-        if (portfolio.isEmpty()) {
-            throw new NotFoundException("Failed to update portfolio with id: " + id);
-        }
+        PortfolioEntity toUpdate = repository.findById(id).orElseThrow(() -> new NotFoundException("Failed to update portfolio with id: " + id));
 
         validate(stocks);
 
-        Portfolio toUpdate = portfolio.get();
         toUpdate.setUpdatedAt(now());
-        toUpdate.setStocks(stocks);
-        return repository.save(toUpdate);
+        toUpdate.setEncryptedStocks(portfolioEncryptService.encryptStocks(stocks.stream()
+            .map(stock -> new PortfolioStock(stock.getCode(), stock.getQuantity()))
+            .toList()));
+        toUpdate.setStocks(null);
+
+        PortfolioEntity updated = repository.save(toUpdate);
+
+        List<PortfolioStock> decryptStocks = portfolioEncryptService.decryptStocks(updated.getEncryptedStocks());
+        updated.setStocks(decryptStocks);
+
+        return convert(updated);
     }
 
     public void delete(String id) {

+ 3 - 0
src/main/resources/application.yml

@@ -12,6 +12,9 @@ clients:
     url: ${inference_client:}
     model: ${inference_model:}
 
+encryption:
+  key: ${encryption_key:}
+
 auth:
   api: ${auth_api:}
 

+ 5 - 1
src/test/java/service/PortfolioServiceTest.java

@@ -6,6 +6,7 @@ import com.danielbohry.stocks.domain.Stock;
 import com.danielbohry.stocks.repository.PortfolioRepository;
 import com.danielbohry.stocks.repository.StockRepository;
 import com.danielbohry.stocks.service.ExchangeService;
+import com.danielbohry.stocks.service.PortfolioEncryptService;
 import com.danielbohry.stocks.service.PortfolioService;
 import com.danielbohry.stocks.service.StockService;
 import org.junit.jupiter.api.AfterEach;
@@ -38,12 +39,15 @@ public class PortfolioServiceTest {
     @Mock
     private ExchangeService exchangeService;
 
+    @Mock
+    private PortfolioEncryptService portfolioEncryptService;
+
     private PortfolioService portfolioService;
 
     @BeforeEach
     public void setup() {
         StockService stockService = new StockService(stockRepository);
-        portfolioService = new PortfolioService(portfolioRepository, stockService, exchangeService);
+        portfolioService = new PortfolioService(portfolioRepository, stockService, exchangeService, portfolioEncryptService);
     }
 
     @AfterEach