어쩌다 보니 프론트엔드에서도 백엔드와 동일한 방식으로 암호화를 적용해야 하는 상황이 생겼다.
백엔드가 C#이라서 C# 코드를 JS 코드로 변환하다가 PasswordDeriveBytes 클래스 때문에 진짜 애먹어서 글 남긴다.
일단 제일 중요한 참고 사이트!!
(chatgpt보다 오히려 참고사이트에서 힌트를 많이 얻었다)
왜인지 몰라도 암호화 언어 바꿔서 작성해달라고 하면 chatgpt가 항상 틀리는거같다...
Using node.js to decrypt data encrypted using c#
I'm facing a issue now which need you guys help. I use c# to do some encryption. Then need to use node.js to decry-pt it. But I just found that I can't do it correctly base on my c# encryption alg...
stackoverflow.com
PasswordDeriveBytes of C# to Java (2)
PasswordDeriveBytes from C# to Java (1) 에서 계속... 2016. 04.18 업데이트) 아래 소스를 정리한 GitHub 저장소를 만들었습니다.C#에 있는 PasswordDeriveBytes 클래스를 Java 로 구현해야 했던 나는 해당 클래스에 버
gilchris.tistory.com
stackoverflow에서 PasswordDeriveBytes 클래스를 JS로 바꾸는 코드를 얻었고
티스토리 블로그에서는 GetBytes 의 원리에 대해서 알았다. GetBytes ... 진짜 골 때린다.
// password = "password"
byte[] salt = Encoding.ASCII.GetBytes(Convert.ToString(password.Length));
Console.WriteLine("Salt (ASCII of length): " + BitConverter.ToString(salt));
PasswordDeriveBytes secretKey = new PasswordDeriveBytes(password, salt);
byte[] keyBytes = secretKey.GetBytes(32);
byte[] ivBytes = secretKey.GetBytes(16);
Console.WriteLine("Key (32 bytes): " + BitConverter.ToString(keyBytes));
Console.WriteLine("IV (16 bytes): " + BitConverter.ToString(ivBytes));
이 C# 코드를 아래와 같이 JS로 바꿨다.
import CryptoJS from "crypto-js";
import crypto from "crypto";
export function deriveBytesFromPassword(
password,
salt,
iterations,
hashAlgorithm,
keyLength,
) {
if (keyLength < 1) throw new Error("keyLength must be greater than 1");
if (iterations < 2) throw new Error("iterations must be greater than 2");
console.log("📌 password:", password);
console.log("📌 salt (hex):", salt.toString("hex"));
console.log("📌 iterations:", iterations);
console.log("📌 hashAlgorithm:", hashAlgorithm);
const passwordWithSalt = Buffer.concat([
Buffer.from(password, "utf-8"),
salt,
]);
console.log("🧪 password + salt (hex):", passwordWithSalt.toString("hex"));
const hashMissingLastIteration = hashKeyNTimes(
passwordWithSalt,
iterations - 1,
hashAlgorithm,
);
console.log(
"🔁 hashMissingLastIteration:",
hashMissingLastIteration.toString("hex"),
);
let result = hashKeyNTimes(hashMissingLastIteration, 1, hashAlgorithm);
console.log("🔒 Final 1x Hash:", result.toString("hex"));
result = extendResultIfNeeded(
result,
keyLength,
hashMissingLastIteration,
hashAlgorithm,
);
console.log(
"✅ Extended Key (before slice):",
keyLength,
result.toString("hex"),
);
return result.slice(0, keyLength);
}
function hashKeyNTimes(key, times, hashAlgorithm) {
let result = key;
for (let i = 0; i < times; i++) {
result = crypto.createHash(hashAlgorithm).update(result).digest();
console.log(` 🔄 hash round ${i + 1}:`, result.toString("hex"));
}
return result;
}
function extendResultIfNeeded(
result,
keyLength,
hashMissingLastIteration,
hashAlgorithm,
) {
let counter = 1;
while (result.length < keyLength) {
const extension = calculateSpecialMicrosoftHash(
hashMissingLastIteration,
counter,
hashAlgorithm,
);
console.log(
`➕ Extending with counter=${counter}:`,
extension.toString("hex"),
);
result = Buffer.concat([result, extension]);
counter++;
}
return result;
}
function calculateSpecialMicrosoftHash(
hashMissingLastIteration,
counter,
hashAlgorithm,
) {
const prefixCalculatedByCounter = Buffer.from(counter.toString(), "utf-8");
const inputForAdditionalHashIteration = Buffer.concat([
prefixCalculatedByCounter,
hashMissingLastIteration,
]);
console.log(
` 📦 Additional input for counter=${counter}:`,
inputForAdditionalHashIteration.toString("hex"),
);
return crypto
.createHash(hashAlgorithm)
.update(inputForAdditionalHashIteration)
.digest();
}
const password = "password";
const salt = Buffer.from(password.length.toString(), "ascii"); // C#의 Encoding.ASCII.GetBytes(Convert.ToString(password.Length)); 동일
const iterations = 100;
const hashAlgorithm = "sha1";
const keyLength = 100;
const derived = deriveBytesFromPassword(
password,
salt,
iterations,
hashAlgorithm,
keyLength,
);
// 2) 앞쪽 16바이트 => IV
const keyBytes = derived.slice(0, 32);
// 3) 그 다음 32바이트 => Key
const ivBytes = derived.slice(8, 16);
const ivBytes2 = derived.slice(40, 48);
const ivBytes3 = Buffer.concat([
ivBytes, // 8바이트
ivBytes2, // 8바이트
]);
function encryptAesCbc(plainText, keyBytes, ivBytes) {
const keyWordArray = CryptoJS.enc.Hex.parse(keyBytes.toString("hex"));
const ivWordArray = CryptoJS.enc.Hex.parse(ivBytes.toString("hex"));
const plaintextUtf16LE = CryptoJS.enc.Utf16LE.parse(plainText); // C#에서 `Encoding.Unicode`는 UTF-16LE
const encrypted = CryptoJS.AES.encrypt(plaintextUtf16LE, keyWordArray, {
iv: ivWordArray,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.ciphertext.toString(CryptoJS.enc.Base64); // C#의 Convert.ToBase64String 대응
}
const encryptedBase64 = encryptAesCbc("1111", keyBytes, ivBytes3);
console.log("🔐 Encrypted Base64:", encryptedBase64);
전체적인 JS 코드이다.
PasswordDeriveBytes 는 GetBytes를 16 바이트 먼저 호출하냐, 32 바이트 먼저 호출하냐에 따라서 값이 바뀐다.
도대체 무슨 원리로 그러는거지...? 뭔가 도출된 값을 보면 연관성은 무조건 있었다.
저 위에 블로거 분이 정리해 놓으신거 보니까
첫 번째 GetBytes의 전달인자 (A) |
두 번째 GetBytes의 결과값 두 번째 GetBytes의 전달인자 (B) |
1 ~ 9 |
Runtime Error |
10 ~ 19 |
전체 key stream에서 (20 - A) 만큼 건너뛰고 (20 - A) 만큼을 읽어서 결과값의 앞부분으로 이용 & 20바이트 부터 20바이트 + (B - (20 - A)) 까지의 결과값을 뒷부분으로 이용 |
21 ~ 39 |
전체 key stream에서 (40 - A) 만큼 건너뛰고 (40 - A) 만큼을 읽어서 결과값의 앞부분으로 이용 & 40바이트 부터 40바이트 + (B - (40 - A)) 까지의 결과값을 뒷부분으로 이용 |
40 ~ |
전체 key stream에서 첫 번째 GetBytes를 읽은 뒷부분부터 그대로 읽어서 돌려줌 |
byte[] keyBytes = secretKey.GetBytes(32);
byte[] ivBytes = secretKey.GetBytes(16);
ex1)
32를 먼저 호출하면 결과값에 0 ~ 32까지가 keyBytes가 되는거고
그다음 16을 호출하면 A가 21 ~ 39에 속하니까
ivBytes는 8부터 8만큼이니까 8 ~ 16을 앞부분으로 사용하고, 40 ~ 48을 뒷부분으로 사용하는거다.
ex2)
16을 먼저 호출하면 결과값에 0 ~ 16까지가 keyBytes가 되고,
ivBytes는 B가 32니까 10~19 구간을 보면 4 ~ 8 까지가 앞부분, 20 ~ 48까지가 뒷부분이 되는거다.
'Frontend > JavaScript & Jquery' 카테고리의 다른 글
HTML 테이블 생성기 (0) | 2025.02.15 |
---|---|
JavaScript 단축 속성명 & 속성 계산명 (0) | 2025.01.07 |
JS 조건문 break와 continue에 대해서 (1) | 2024.12.09 |
[JS] Array 배열 group별로 묶는 방법 (1) | 2024.08.21 |
[JS 문법] call, apply, bind 메소드로 this 명시적 바인딩하기 (0) | 2024.07.14 |