에이전트 ARIA 키 관리 기능 #4

- 진행중
main
icksishu@gmail.com 4 weeks ago
parent 4e1e896db9
commit 624e6095be

@ -173,20 +173,21 @@ COMMENT ON COLUMN TB_DFX_AGENT_MESSAGE_HISTORY.PROCESS_ACK_TS IS '처리결과
CREATE TABLE TB_DFX_CRYPTO_KEY ( CREATE TABLE TB_DFX_CRYPTO_KEY (
KEY_VALUE VARCHAR(256) NOT NULL KEY_ID VARCHAR(256) NOT NULL
, HASH_VALUE VARCHAR(256) NOT NULL , KEY_TYPE_NAME VARCHAR(32) NOT NULL
, ALGORITHM_NAME VARCHAR(64) NOT NULL , ALGORITHM_NAME VARCHAR(64) NOT NULL
, MODE_NAME VARCHAR(64) NOT NULL
, TARGET_AGENT_HOST_ID VARCHAR(256) , TARGET_AGENT_HOST_ID VARCHAR(256)
, APPLY_TS TIMESTAMPTZ(3) , APPLY_TS TIMESTAMPTZ(3)
, DATA_ENCRYPTION_YN VARCHAR(1) , DATA_ENCRYPTION_YN VARCHAR(1)
, JSON_WEB_KEY TEXT
, CONSTRAINT PK_DFX_CRYPTO_KEY PRIMARY KEY (KEY_ID)
); );
COMMENT ON TABLE TB_DFX_CRYPTO_KEY IS '암호화키정보'; COMMENT ON TABLE TB_DFX_CRYPTO_KEY IS '암호화키정보';
COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.KEY_VALUE IS 'KEY VALUE'; COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.KEY_ID IS 'KEY ID';
COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.HASH_VALUE IS 'HASH VALUE'; COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.KEY_TYPE_NAME IS '대칭키(oct), RSA공개/개인키(RSA), 타원곡선키(EC), Ed25519(OKP)';
COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.ALGORITHM_NAME IS '알고리즘'; COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.ALGORITHM_NAME IS '알고리즘';
COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.MODE_NAME IS '모드';
COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.TARGET_AGENT_HOST_ID IS '적용 에이전트 ID'; COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.TARGET_AGENT_HOST_ID IS '적용 에이전트 ID';
COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.APPLY_TS IS '적용 시간'; COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.APPLY_TS IS '적용 시간';
COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.DATA_ENCRYPTION_YN IS '데이터 암호화 여부'; COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.DATA_ENCRYPTION_YN IS '데이터 암호화 여부';
COMMENT ON COLUMN TB_DFX_CRYPTO_KEY.JSON_WEB_KEY IS 'JWT 키 데이터';

@ -1,6 +1,7 @@
<script setup> <script setup>
import '@/assets/main.css' import '@/assets/main.css'
import { useApiClient } from '@/components/apiClient' import { useApiClient } from '@/components/apiClient'
import { Modal } from 'bootstrap'
import { onMounted, ref, reactive } from 'vue' import { onMounted, ref, reactive } from 'vue'
const apiClient = useApiClient() const apiClient = useApiClient()
@ -10,14 +11,53 @@ async function getCryptoKeyDtoList() {
return response.data return response.data
} }
let dfxCryptoKeyDtoList = ref([]) const newKeyModalEl = ref(null)
const cryptoKeyDto = reactive({}) let newKeyModalInstance = null
const isOpenedNewKeyModal = ref(false)
function openNewKeyModal() {
dfxCryptoKeyDto.keyword = ''
isOpenedNewKeyModal.value = true
newKeyModalInstance?.show()
}
function closeNewKeyModal() {
newKeyModalInstance?.hide()
}
const dfxCryptoKeyDto = reactive({
keyword: '',
keyId: '',
keyTypeName: 'oct',
algorithmName: 'ARIA-256-GCM',
targetAgentHostId: '',
applyTs: 0,
dataEncryptionYn: 'N',
})
async function generateNewKey() {} function generateNewKey() {
if (dfxCryptoKeyDto.keyword && confirm(dfxCryptoKeyDto.keyword + '의 데이터 암호화 키를 생성하시겠습니까?')) {
apiClient
.post('/app-api/agent/saveCryptoKeyDto', dfxCryptoKeyDto)
.then((response) => {
getCryptoKeyDtoList()
})
.catch((error) => {
console.error(error)
debugger
alert('데이터 암호화 키 생성 중 오류가 발생하였습니다.')
})
closeNewKeyModal()
}
}
async function saveList() {} async function saveList() {}
let dfxCryptoKeyDtoList = ref([])
const cryptoKeyDto = reactive({})
onMounted(async () => { onMounted(async () => {
newKeyModalInstance = new Modal(newKeyModalEl.value, { backdrop: 'static', keyboard: false })
newKeyModalInstance.hide()
dfxCryptoKeyDtoList.value = await getCryptoKeyDtoList() dfxCryptoKeyDtoList.value = await getCryptoKeyDtoList()
}) })
</script> </script>
@ -33,16 +73,15 @@ onMounted(async () => {
<article class="col-12 pt-3"> <article class="col-12 pt-3">
<div class="d-flex flex-row-reverse"> <div class="d-flex flex-row-reverse">
<button type="button" class="btn btn-primary btn-sm m-1" @click="saveList">Save</button> <button type="button" class="btn btn-primary btn-sm m-1" @click="saveList">Save</button>
<button type="button" class="btn btn-success btn-sm m-1" @click="generateNewKey">Generate new Key</button> <button type="button" class="btn btn-success btn-sm m-1" @click="openNewKeyModal">Generate new Key</button>
</div> </div>
<table class="table table-striped table-bordered align-middle"> <table class="table table-striped table-bordered align-middle">
<thead> <thead>
<tr> <tr>
<th scope="col" class="text-center">선택</th> <th scope="col" class="text-center">선택</th>
<th scope="col" class="text-center">Key</th> <th scope="col" class="text-center">Key ID</th>
<th scope="col" class="text-center">Hash</th> <th scope="col" class="text-center">Type</th>
<th scope="col" class="text-center">Algorithm</th> <th scope="col" class="text-center">Algorithm</th>
<th scope="col" class="text-center">Mode</th>
<th scope="col" class="text-center">Target agent</th> <th scope="col" class="text-center">Target agent</th>
<th scope="col" class="text-center">Applied at</th> <th scope="col" class="text-center">Applied at</th>
<th scope="col" class="text-center">Data encryption</th> <th scope="col" class="text-center">Data encryption</th>
@ -51,10 +90,9 @@ onMounted(async () => {
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr v-if="dfxCryptoKeyDtoList.length > 0" v-for="(cryptoKeyDto, index) in dfxCryptoKeyDtoList" :key="index"> <tr v-if="dfxCryptoKeyDtoList.length > 0" v-for="(cryptoKeyDto, index) in dfxCryptoKeyDtoList" :key="index">
<td scope="row" class="text-center"><input type="checkbox" class="form-check-input" v-model="cryptoKeyDto.selected" /></td> <td scope="row" class="text-center"><input type="checkbox" class="form-check-input" v-model="cryptoKeyDto.selected" /></td>
<td class="text-center">{{ cryptoKeyDto.keyValue }}</td> <td class="text-center">{{ cryptoKeyDto.keyId }}</td>
<td class="text-center">{{ cryptoKeyDto.hashValue }}</td> <td class="text-center">{{ cryptoKeyDto.keyTypeName }}</td>
<td class="text-center">{{ cryptoKeyDto.algorithmName }}</td> <td class="text-center">{{ cryptoKeyDto.algorithmName }}</td>
<td class="text-center">{{ cryptoKeyDto.modeName }}</td>
<td class="text-center">{{ cryptoKeyDto.targetAgentHostId }}</td> <td class="text-center">{{ cryptoKeyDto.targetAgentHostId }}</td>
<td class="text-center">{{ cryptoKeyDto.applyTs }}</td> <td class="text-center">{{ cryptoKeyDto.applyTs }}</td>
<td class="text-center">{{ cryptoKeyDto.dataEncryptionYn }}</td> <td class="text-center">{{ cryptoKeyDto.dataEncryptionYn }}</td>
@ -67,6 +105,53 @@ onMounted(async () => {
</article> </article>
</div> </div>
</main> </main>
<!-- 암호화 생성 또는 정보 저장 -->
<div class="modal fade" ref="newKeyModalEl" id="crypto-key-manage-view-input-form-modal" tabindex="-1" aria-labelledby="crypto-key-manage-view-input-form-modal-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header justify-content-between">
<h1 class="modal-title fs-5 pt-0" id="crypto-key-manage-view-input-form-modal-label">데이터 암호화 생성</h1>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-success btn-sm" @click.prevent="generateNewKey">생성</button>
<button type="button" class="btn-close" aria-label="Close" @click.prevent="closeNewKeyModal"></button>
</div>
</div>
<div class="modal-body">
<div class="form-wrapper">
<div class="row align-items-center">
<div class="col-3">
<label for="crypto-key-manage-view-new-keyword" class="col-form-label">Keyword</label>
</div>
<div class="col-auto">
<input type="text" id="crypto-key-manage-view-new-keyword" class="form-control" aria-describedby="crypto-key-manage-view-new-keyword-help-line" maxlength="32" v-model="dfxCryptoKeyDto.keyword" />
</div>
<div class="col-auto">
<p id="crypto-key-manage-view-new-keyword-help-line" class="form-text">관리를 위한 ID/이름 등의 키워드를 입력하여 주십시오.</p>
<p class="form-text">암호화키는 무작위로 생성됩니다.</p>
</div>
</div>
<div class="row align-items-center">
<div class="col-3">
<label for="crypto-key-manage-view-new-key-type-name" class="col-form-label"> 타입</label>
</div>
<div class="col-auto">
<input type="text" id="crypto-key-manage-view-new-key-type-name" class="form-control" readonly v-model="dfxCryptoKeyDto.keyTypeName" />
</div>
</div>
<div class="row align-items-center">
<div class="col-3">
<label for="crypto-key-manage-view-new-algorithm-name" class="col-form-label">알고리즘</label>
</div>
<div class="col-auto">
<input type="text" id="crypto-key-manage-view-new-algorithm-name" class="form-control" readonly v-model="dfxCryptoKeyDto.algorithmName" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<style></style> <style></style>

@ -12,6 +12,8 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -92,4 +94,10 @@ public class DfxAgentInfoController {
List<DfxCryptoKeyDto> dfxCryptoKeyDtoList = dfxCryptoKeyService.selectDfxCryptoKeyList(); List<DfxCryptoKeyDto> dfxCryptoKeyDtoList = dfxCryptoKeyService.selectDfxCryptoKeyList();
return ResponseEntity.ok().body(dfxCryptoKeyDtoList); return ResponseEntity.ok().body(dfxCryptoKeyDtoList);
} }
@PostMapping("/app-api/agent/saveCryptoKeyDto")
public ResponseEntity<DfxCryptoKeyDto> saveCryptoKeyDto(@RequestBody DfxCryptoKeyDto dfxCryptoKeyDto) throws NoSuchAlgorithmException, NoSuchProviderException {
dfxCryptoKeyDto = dfxCryptoKeyService.saveDfxCryptoKey(dfxCryptoKeyDto);
return ResponseEntity.ok().body(dfxCryptoKeyDto);
}
} }

@ -9,11 +9,12 @@ import lombok.*;
@Builder @Builder
@ToString @ToString
public class DfxCryptoKeyDto { public class DfxCryptoKeyDto {
private String keyValue; private String keyword;
private String hashValue; private String keyId;
private String keyTypeName;
private String algorithmName; private String algorithmName;
private String modeName;
private String targetAgentHostId; private String targetAgentHostId;
private long applyTs; private long applyTs;
private String dataEncryptionYn; private String dataEncryptionYn;
private String jsonWebKey;
} }

@ -9,7 +9,7 @@ import java.util.List;
public interface DfxCryptoKeyMapper { public interface DfxCryptoKeyMapper {
List<DfxCryptoKeyDto> selectDfxCryptoKeyList(SearchParameterDto searchParameterDto); List<DfxCryptoKeyDto> selectDfxCryptoKeyList(SearchParameterDto searchParameterDto);
int selectDfxCryptoKeyListTotalCount(SearchParameterDto searchParameterDto); int selectDfxCryptoKeyListTotalCount(SearchParameterDto searchParameterDto);
DfxCryptoKeyDto selectDfxCryptoKeyByKeyValue(DfxCryptoKeyDto dfxCryptoKeyDto); DfxCryptoKeyDto selectDfxCryptoKeyByKeyId(DfxCryptoKeyDto dfxCryptoKeyDto);
void insertDfxCryptoKey(DfxCryptoKeyDto dfxCryptoKeyDto); void insertDfxCryptoKey(DfxCryptoKeyDto dfxCryptoKeyDto);
int updateDfxCryptoKeyToApply(DfxCryptoKeyDto dfxCryptoKeyDto); int updateDfxCryptoKeyToApply(DfxCryptoKeyDto dfxCryptoKeyDto);
} }

@ -14,12 +14,19 @@ import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.UUID;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class DfxCryptoKeyService { public class DfxCryptoKeyService {
static {
if(Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private final DfxCryptoKeyMapper cryptoKeyMapper; private final DfxCryptoKeyMapper cryptoKeyMapper;
public List<DfxCryptoKeyDto> selectDfxCryptoKeyList() { public List<DfxCryptoKeyDto> selectDfxCryptoKeyList() {
@ -27,13 +34,55 @@ public class DfxCryptoKeyService {
return dfxCryptoKeyDtoList; return dfxCryptoKeyDtoList;
} }
public DfxCryptoKeyDto generateNewKey() throws NoSuchAlgorithmException, NoSuchProviderException { /**
DfxCryptoKeyDto dfxCryptoKeyDto = DfxCryptoKeyDto.builder().build(); * TB_DFX_CRYPTO_KEY . DfxCryptoKeyDto.keyId DfxCryptoKeyDto.keyword .
Security.addProvider(new BouncyCastleProvider()); */
KeyGenerator keyGenerator = KeyGenerator.getInstance("ARIA", "BC"); public DfxCryptoKeyDto saveDfxCryptoKey(DfxCryptoKeyDto dfxCryptoKeyDto) throws NoSuchAlgorithmException, NoSuchProviderException {
keyGenerator.init(256, new SecureRandom()); DfxCryptoKeyDto existDfxCryptoKeyDto = cryptoKeyMapper.selectDfxCryptoKeyByKeyId(dfxCryptoKeyDto);
SecretKey key = keyGenerator.generateKey(); if(existDfxCryptoKeyDto == null) {
String keyId = this.generateKeyIdByKeyword(dfxCryptoKeyDto.getKeyword());
dfxCryptoKeyDto.setKeyId(keyId);
String jsonWebKey = this.generateNewKey(keyId);
dfxCryptoKeyDto.setJsonWebKey(jsonWebKey);
cryptoKeyMapper.insertDfxCryptoKey(dfxCryptoKeyDto);
}
else {
cryptoKeyMapper.updateDfxCryptoKeyToApply(dfxCryptoKeyDto);
}
return dfxCryptoKeyDto; return dfxCryptoKeyDto;
} }
private String generateKeyIdByKeyword(String keyword) {
String keyId = keyword + "-" + UUID.randomUUID().toString();
keyId = keyId.length() > 256 ? keyId.substring(0, 256) : keyId;
return keyId;
}
/**
* : jwk - JSON Web Key (RFC7517)
* {
* "kid": "Key ID. 이 json object 에 대한 unique ID",
* "kty": "Key TYpe. oct(Octet sequence (대칭키). ARIA, AES, HMAC) ",
* "alg": "ALGorithm, DFX에서 지원하는 범위: ARIA-256-GCM",
* "k": "실제 키의 바이트(Base64 인코딩)"
* }
* @return jwk
*/
private String generateNewKey(String keyId) {
byte[] key = new byte[32];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(key); // random key 생성
String encodedKey = Base64.getEncoder().encodeToString(key);
String jwk = """
{
"kid": "%s",
"kty": "oct",
"alg": "ARIA-256-GCM",
"k": "%s"
}
""".formatted(keyId, encodedKey);
return jwk;
}
} }

@ -4,14 +4,14 @@
<select id="selectDfxCryptoKeyList" resultType="com.bsmlab.dfx.dfxconsole.app.agent.service.DfxCryptoKeyDto"> <select id="selectDfxCryptoKeyList" resultType="com.bsmlab.dfx.dfxconsole.app.agent.service.DfxCryptoKeyDto">
<![CDATA[ <![CDATA[
SELECT A.KEY_VALUE, A.HASH_VALUE, A.ALGORITHM_NAME, A.MODE_NAME, A.TARGET_AGENT_HOST_ID SELECT A.KEY_ID, A.KEY_TYPE_NAME, A.ALGORITHM_NAME, A.TARGET_AGENT_HOST_ID
, (EXTRACT(EPOCH FROM A.APPLY_TS) * 1000)::BIGINT AS APPLY_TS , (EXTRACT(EPOCH FROM A.APPLY_TS) * 1000)::BIGINT AS APPLY_TS
, A.DATA_ENCRYPTION_YN , A.DATA_ENCRYPTION_YN
FROM ( FROM (
SELECT KEY_VALUE, HASH_VALUE, ALGORITHM_NAME, MODE_NAME, TARGET_AGENT_HOST_ID, APPLY_TS, DATA_ENCRYPTION_YN SELECT KEY_ID, KEY_TYPE_NAME, ALGORITHM_NAME, TARGET_AGENT_HOST_ID, APPLY_TS, DATA_ENCRYPTION_YN
FROM TB_DFX_CRYPTO_KEY FROM TB_DFX_CRYPTO_KEY
WHERE 1 = 1 WHERE 1 = 1
ORDER BY TARGET_AGENT_HOST_ID, KEY_VALUE ORDER BY TARGET_AGENT_HOST_ID, KEY_ID
LIMIT #{itemCountPerPage} LIMIT #{itemCountPerPage}
OFFSET (#{page} - 1) * #{itemCountPerPage} OFFSET (#{page} - 1) * #{itemCountPerPage}
) A ) A
@ -23,7 +23,7 @@
<![CDATA[ <![CDATA[
SELECT COUNT(*) SELECT COUNT(*)
FROM ( FROM (
SELECT KEY_VALUE, HASH_VALUE, ALGORITHM_NAME, MODE_NAME, TARGET_AGENT_HOST_ID, APPLY_TS, DATA_ENCRYPTION_YN SELECT KEY_ID, KEY_TYPE_NAME, ALGORITHM_NAME, TARGET_AGENT_HOST_ID, APPLY_TS, DATA_ENCRYPTION_YN
FROM TB_DFX_CRYPTO_KEY FROM TB_DFX_CRYPTO_KEY
WHERE 1 = 1 WHERE 1 = 1
) A ) A
@ -31,37 +31,41 @@
]]> ]]>
</select> </select>
<select id="selectDfxCryptoKeyByKeyValue" parameterType="com.bsmlab.dfx.dfxconsole.app.agent.service.DfxCryptoKeyDto" resultType="com.bsmlab.dfx.dfxconsole.app.agent.service.DfxCryptoKeyDto"> <select id="selectDfxCryptoKeyByKeyId" parameterType="com.bsmlab.dfx.dfxconsole.app.agent.service.DfxCryptoKeyDto" resultType="com.bsmlab.dfx.dfxconsole.app.agent.service.DfxCryptoKeyDto">
<![CDATA[ <![CDATA[
SELECT KEY_VALUE, HASH_VALUE, ALGORITHM_NAME, MODE_NAME, TARGET_AGENT_HOST_ID, (EXTRACT(EPOCH FROM APPLY_TS) * 1000)::BIGINT AS APPLY_TS, DATA_ENCRYPTION_YN SELECT KEY_ID, KEY_TYPE_NAME, ALGORITHM_NAME, TARGET_AGENT_HOST_ID, (EXTRACT(EPOCH FROM APPLY_TS) * 1000)::BIGINT AS APPLY_TS, DATA_ENCRYPTION_YN
, JSON_WEB_KEY
FROM TB_DFX_CRYPTO_KEY FROM TB_DFX_CRYPTO_KEY
WHERE 1 = 1 WHERE 1 = 1
AND KEY_VALUE = #{keyValue} AND KEY_ID = #{keyId}
]]> ]]>
</select> </select>
<insert id="insertDfxCryptoKey" parameterType="com.bsmlab.dfx.dfxconsole.app.agent.service.DfxCryptoKeyDto"> <insert id="insertDfxCryptoKey" parameterType="com.bsmlab.dfx.dfxconsole.app.agent.service.DfxCryptoKeyDto">
<![CDATA[ <![CDATA[
INSERT INTO TB_DFX_AGENT_MESSAGE ( INSERT INTO TB_DFX_CRYPTO_KEY (
KEY_VALUE, HASH_VALUE, ALGORITHM_NAME, MODE_NAME, TARGET_AGENT_HOST_ID KEY_ID, KEY_TYPE_NAME, ALGORITHM_NAME, TARGET_AGENT_HOST_ID
, APPLY_TS , APPLY_TS
, DATA_ENCRYPTION_YN , DATA_ENCRYPTION_YN
, JSON_WEB_KEY
) )
VALUES ( VALUES (
#{keyValue}, #{hashValue}, #{algorithmName}, #{modeName}, #{targetAgentHostId} #{keyId}, #{keyTypeName}, #{algorithmName}, #{targetAgentHostId}
, #{applyTs, jdbcType=TIMESTAMP_WITH_TIMEZONE, javaType=Long} , #{applyTs, jdbcType=TIMESTAMP_WITH_TIMEZONE, javaType=Long}
, #{dataEncryptionYn} , #{dataEncryptionYn}
, #{jsonWebKey}
)
]]> ]]>
</insert> </insert>
<update id="updateDfxCryptoKeyToApply" parameterType="com.bsmlab.dfx.dfxconsole.app.agent.service.DfxCryptoKeyDto"> <update id="updateDfxCryptoKeyToApply" parameterType="com.bsmlab.dfx.dfxconsole.app.agent.service.DfxCryptoKeyDto">
<![CDATA[ <![CDATA[
UPDATE TB_DFX_AGENT_MESSAGE UPDATE TB_DFX_CRYPTO_KEY
SET TARGET_AGENT_HOST_ID = #{targetAgentHostId} SET TARGET_AGENT_HOST_ID = #{targetAgentHostId}
, APPLY_TS = NOW() , APPLY_TS = NOW()
, DATA_ENCRYPTION_YN = #{dataEncryptionYn} , DATA_ENCRYPTION_YN = #{dataEncryptionYn}
WHERE 1 = 1 WHERE 1 = 1
KEY_VALUE = #{keyValue} KEY_ID = #{keyValue}
]]> ]]>
</update> </update>

Loading…
Cancel
Save