[암호화] C# PasswordDeriveBytes 를 JS 코드로 변환하기

2025. 4. 15. 20:01·Frontend/JavaScript & Jquery

어쩌다 보니 프론트엔드에서도 백엔드와 동일한 방식으로 암호화를 적용해야 하는 상황이 생겼다.

백엔드가 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
'Frontend/JavaScript & Jquery' 카테고리의 다른 글
  • HTML 테이블 생성기
  • JavaScript 단축 속성명 & 속성 계산명
  • JS 조건문 break와 continue에 대해서
  • [JS] Array 배열 group별로 묶는 방법
전예방
전예방
  • 전예방
    예방이의 개발일기
    전예방
  • 전체
    오늘
    어제
    • All (125)
      • Info & Tip (2)
      • 유용한 사이트들 (5)
      • Mark Up & StyleSheet (23)
        • HTML (6)
        • CSS & SCSS (10)
        • 반응형 (6)
      • Frontend (66)
        • 전체 (10)
        • JavaScript & Jquery (18)
        • TypeScript (0)
        • React (26)
        • Next.js (3)
        • 성능최적화 (2)
        • 웹접근성 (2)
      • Backend (13)
        • Python (1)
        • JAVA (2)
        • node.js (0)
        • PHP (7)
        • 패키지매니저 (3)
      • Markdown (0)
      • SCM (1)
        • Git&Github (1)
        • SVN (0)
      • IDE (4)
        • VSCode (2)
        • IntelliJ (1)
        • Ecplise (1)
      • 취미생활 (3)
      • 정보처리기사 (2)
      • 코딩자율학습단 (5)
        • 12기 (파이썬) (5)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • velog
    • github
  • 공지사항

  • 인기 글

  • 태그

    React Swiper
    CSS
    frontend
    반응형
    리액트
    나도코딩
    파이썬
    php
    TypeScript
    Python
    코딩자율학습
    Chart.js
    react
    swiper.js
    Admin
    관리자 페이지
    코딩자율학습단
    NPM
    yarn berry
    회원가입
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.3
전예방
[암호화] C# PasswordDeriveBytes 를 JS 코드로 변환하기
상단으로

티스토리툴바