2022. 7. 29. 15:44ㆍJava/Basic
- HMAC
- 해시 함수와 HMAC 의 코드 비교
1. HMAC
Keyed-Hashing for Message Authentication Codes
메시지 변조 여부를 확인하는 MAC에 해싱을 접목한 기술입니다.
비밀키(= 대칭키)를 이용하여 digest를 생성합니다.
대칭키 알고리즘과 해시 알고리즘과는 아래와 같은 차이점이 있습니다.
- 대칭키 알고리즘은 비밀키를 이용하여 원문으로 복호화가 가능하지만, MAC은 원문으로 복호화할 수는 없습니다.
- 단방향 해시 알고리즘은 비밀키 없이 digest를 생성하지만, MAC은 digest 생성시 비밀키가 반드시 필요합니다.
digest의 무결성 검사는 비밀키를 아는 사람만이 할 수 있고, 무결성 검사는 해시 알고리즘 처럼 해싱한 값의 일치여부로 판단합니다.
메시지 내용에서 일부가 바뀔경우 digest값이 크게 바뀌는 Avalanche effect(산사태 효과)가 단방향 해시 알고리즘의 특성과 동일하게 적용되기 때문에 공격자가 임의로 메시지 변조를 할 수 없습니다.
HMAC 은 API의 request 변조를 막는데에 좋습니다.
- client 측에서는 server에 request를 전송할 때 전달하고자 하는 메시지 원문과 메시지 원문을 HMAC으로 암호화한 digest를 함께 전송합니다.
- server 측에서는 전달 받은 메시지 원문을 SECRET_KEY를 이용하여 HMAC 변환한 값과 digest 의 일치여부를 체크하여 메시지의 변조 여부를 검증합니다.
HMAC 종류로는 HmacMD5, HmacSHA1, HmacSHA256, HmacSHA512, HmacPBESHA256, HmacPBESHA512, PBEWithHmacSHA256
등이 있습니다.
2. 해시 함수와 HMAC 의 코드 비교
2.1. 해시 함수 - SHA-256
단방향 해시 알고리즘은 java.security.MessageDigest
클래스를 이용하여 객체를 생성하며, 별도의 키 없이 해시를 생성할 수 있습니다.
해시 알고리즘의 특성 상, 원문이 조금만 수정되어도 해시값이 완전 바뀝니다.
※ 해시 알고리즘에 대한 자세한 설명을 보고 싶다면 [JCA] Hash 함수의 개요와 PBKDF2를 이용한 단방향 해시 알고리즘 구현 포스팅을 참고해주세요.
public class Sha256Example { public static void main(String[] args) { String data = "hello jiniworld", data2 = "hello jiniworld!"; System.out.println(encrypt(data)); System.out.println(encrypt(data)); System.out.println(encrypt(data2)); } static String encrypt(String message) { try { final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); messageDigest.update(message.getBytes()); return Base64.getEncoder().encodeToString(messageDigest.digest()); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }
fBWgim+2DAp0+01x5Ylpr9pPFC7zbx5GMmofaw5+sGI= fBWgim+2DAp0+01x5Ylpr9pPFC7zbx5GMmofaw5+sGI= A7Z8clE8u2kBxXxYgArInfN49/VKiOjFXw5zXXvW74g=
2.2. HMAC - HmacSHA256
MAC + SHA-256 으로 암호화한 예제입니다.
일반 단방향 암호화 알고리즘과 차이점으로, 비밀키를 입력받습니다. (line 18)
동일한 문자열에 대해서 동일한 해시값을 출력하며, MessageDigest와 마찬가지로 원문에서 조금만 수정되어도 해시값이 완전 바뀝니다.
public class HmacExample { private static final String ALGORITHM = "HmacSHA256"; private static final SecretKeySpec SECRET_KEY_SPEC; static { SECRET_KEY_SPEC = new SecretKeySpec("secretKe!y@@98".getBytes(), ALGORITHM); } public static void main(String[] args) { String message = "hello jiniworld", message2 = "hello jiniworld!";; System.out.println(encrypt(message)); System.out.println(encrypt(message)); System.out.println(encrypt(message2)); } public static String encrypt(String message) { try { Mac mac = Mac.getInstance(ALGORITHM); mac.init(SECRET_KEY_SPEC); mac.update(message.getBytes());- return Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes())); } catch (Exception e) { e.printStackTrace(); } return null; } }
Ic29u6OwnxcnpV4ePBeLCewGU3kM8XbLznUjWCL5dPI= Ic29u6OwnxcnpV4ePBeLCewGU3kM8XbLznUjWCL5dPI= eyhcKY6i5iJkuz6iw99rj3SzKkpmR38brDlYyymUYAY=
3. HMAC을 이용한 메시지 검증 예제
위에 정의한 Hmac encrypt 메서드를 이용하여 API 메시지 검증을 해보도록 합시다.
- 아래의 POST body값으로 userId, name을 받는다.
- Basic 인증방식을 이용하며, Basic 토큰 내에 userId를 HMAC으로 변환한 값을 넣는다.
- API server 측에서는 Request Body에 들어있는 userId값과 Basic토큰 내에 들어있는 메시지인증코드를 비교하여 request가 변조되었는지를 체크한다.
@NoArgsConstructor @Getter @Setter public class HmacTestValue { private String userId; private String name; public HmacTestValue(String userId, String name) { this.userId = userId; this.name = name; } }
아주 간략하게, 메시지 검증을 하는 API를 만들어보았습니다.
만일 메시지가 변조되지 않았을 경우, 메시지 검증 성공!! {이름}
형식을 출력하고, 변조된 경우에는 Authorization 정보가 잘못되었습니다
가 출력됩니다.
@RequestMapping("/crypto") @RestController public class CryptoController { @PostMapping("/hmac") public String hmac(@RequestHeader("Authorization") String authorization, @RequestBody HmacTestValue req) { if(Strings.isNotBlank(authorization)) { authorization = authorization.replaceAll("^Basic ", ""); } if (authorization.equals(HmacExample.encrypt(req.getUserId()))) { return "메시지 검증 성공!! " + req.getName(); } else { return "Authorization 정보가 잘못되었습니다"; } } }
Test코드를 추가했습니다.
첫번째 RestTemplate에서는 변조되지 않은 상태를 가정하고 넣었고
두번째 RestTemplate에서는 변조된 상황을 가정하여 예문을 작성했습니다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class CryptoControllerTest { @LocalServerPort int port; RestTemplate client = new RestTemplate(); @DisplayName("1. Hmac 메시지 검증 테스트") @Test void hmacTest() { // AUTHORIZATION 에 이용된 HMAC 암호화값과 RequestBody에 들어있는 userId 가 일치한 경우 String userId = "U001"; HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.AUTHORIZATION, "Basic "+ HmacExample.encrypt(userId)); HmacTestValue body = new HmacTestValue(userId, "jini"); HttpEntity entity = new HttpEntity(body, headers); ResponseEntity<String> resp = client.exchange("http://localhost:"+port+"/crypto/hmac", HttpMethod.POST, entity, String.class); System.out.println(resp.getBody()); // AUTHORIZATION 에 이용된 HMAC 암호화값과 RequestBody에 들어있는 userId 가 일치하지 않은 경우 HmacTestValue body2 = new HmacTestValue("U002", "jini"); HttpEntity entity2 = new HttpEntity(body2, headers); ResponseEntity<String> resp2 = client.exchange("http://localhost:"+port+"/crypto/hmac", HttpMethod.POST, entity2, String.class); System.out.println(resp2.getBody()); } }
출력 결과는 아래와 같습니다.
메시지 검증 성공!! jini Authorization 정보가 잘못되었습니다
'Java > Basic' 카테고리의 다른 글
[Java] Functional Programming - 1. Java 함수형 프로그래밍의 개요와 Stream (1) | 2022.12.08 |
---|---|
[Java] record class Mocking 에러 해결하기 (0) | 2022.12.02 |
[JCA] Hash 함수의 개요와 PBKDF2를 이용한 단방향 해시 알고리즘 구현 (2) | 2022.07.27 |
[JCA] Cipher 클래스를 이용한 AES 대칭키 암복호화 (0) | 2022.07.22 |
[JDK 14] Switch 문에서 arrow operator 이용하기 (0) | 2022.05.06 |