paint-brush
스테가노그래피: JavaScript를 사용하여 이미지에서 텍스트를 숨기는 방법~에 의해@andriiromasiun
1,467 판독값
1,467 판독값

스테가노그래피: JavaScript를 사용하여 이미지에서 텍스트를 숨기는 방법

~에 의해 Andrii Romasiun9m2024/06/26
Read on Terminal Reader

너무 오래; 읽다

스테가노그래피는 비밀이 아닌 다른 파일에 비밀 메시지를 숨기는 방법입니다. 사용자는 이미지를 업로드하여 그 안에 숨겨진 메시지를 읽거나 이미지 자체에 메시지를 인코딩할 수 있습니다. 이 문서에서는 JavaScript를 사용하여 이러한 인코딩 엔진을 구현하는 방법을 설명합니다.
featured image - 스테가노그래피: JavaScript를 사용하여 이미지에서 텍스트를 숨기는 방법
Andrii Romasiun HackerNoon profile picture
0-item

스테가노그래피란 무엇입니까?

친구에게 비밀 메시지를 보내고 싶지만 사용하려는 채널이 손상되어 모니터링되고 있다고 상상해 보세요. 일부 암호화를 사용할 수 있지만 그렇게 하면 대화를 모니터링하는 사람들의 의심을 불러일으킬 수 있으므로 다른 암호화 방법을 사용해야 합니다.

요즘 스테가노그래피는 비밀이 아닌 다른 파일(예: 고양이 사진)에 비밀 메시지를 숨겨 해당 파일을 보내도 감지되지 않도록 하는 방법입니다. 스테가노그래피는 이미지의 텍스트를 숨기는 것에만 국한되지 않고 일반적으로 "비밀이 아닌 다른 메시지나 물리적 개체에 비밀 정보를 숨기는 것"을 의미합니다. 예를 들어 열 전치 등을 사용하여 오디오, 비디오 또는 기타 텍스트에서 일부 메시지를 숨길 수 있습니다.


스테가노그래피는 다른 많은 경우에도 매우 유용할 수 있습니다. 예를 들어, 민감한 문서의 유출을 방지하기 위해 워터마킹에 대한 좋은 대안이 될 수 있습니다.


단순히 파일 끝에 텍스트를 추가하는 것부터 메타데이터에서 정보를 숨기는 것까지 이미지의 정보를 숨기는 방법은 다양합니다. 이 기사에서는 바이너리 수준으로 내려가 이미지 자체의 경계 내에 메시지를 숨기는 고급 스테가노그래피 방법을 다루고 싶습니다.

스테가노그래픽 엔진 구축

UI

스테가노그래피의 예에서는 브라우저에서 실행될 수 있는 강력한 프로그래밍 언어인 JavaScript를 사용하기로 결정했습니다.


나는 사용자가 이미지를 업로드하여 그 안에 숨겨진 메시지를 읽거나 이미지 자체에 메시지를 인코딩할 수 있는 간단한 인터페이스를 생각해냈습니다.


 <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Steganography</title> <style> body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; } textarea { width: 300px; height: 100px; } button { margin: 10px; } #outputImage { margin-top: 20px; max-width: 100%; } </style> </head> <body> <h1>Steganography Example</h1> <input type="file" id="upload" accept="image/*"><br> <canvas id="canvas" style="display:none;"></canvas><br> <textarea id="text" placeholder="Enter text to encode"></textarea><br> <button id="encode">Encode Text</button> <button id="decode">Decode Text</button> <p id="decodedText"></p> <img id="outputImage" alt="Output Image"> <script src="./script.js"></script> </body> </html>


이를 사용하려면 사용자는 조작하려는 이미지를 선택하고 해당 이미지의 일부 텍스트를 디코딩하거나 인코딩하고 나중에 이미지를 다운로드하면 됩니다.



이미지 처리

JavaScript에서 이미지 작업을 하려면 Canvas API를 사용할 수 있습니다. 이미지, 애니메이션, 게임 그래픽 및 비디오를 조작하고 그리는 데 필요한 다양한 기능을 제공합니다.


Canvas API는 주로 2D 그래픽에 사용됩니다. 3D 하드웨어 가속 그래픽으로 작업하려면 WebGL API (부수적으로 <canvas> 요소도 사용함)를 사용할 수 있습니다.


 const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const image = new Image();


파일 시스템에서 이미지 파일을 읽고 캔버스 컨텍스트에 추가하려면 FileReader API를 사용할 수 있습니다. 이를 통해 사용자 정의 라이브러리 없이도 사용자 컴퓨터에 저장된 모든 파일의 내용을 쉽게 읽을 수 있습니다.

 function handleFileUpload(event) { const reader = new FileReader(); reader.onload = function (e) { image.src = e.target.result; image.onload = function () { canvas.width = image.width; canvas.height = image.height; ctx.drawImage(image, 0, 0); }; }; reader.readAsDataURL(event.target.files[0]); }


파일을 읽고 해당 파일의 이미지를 이전에 정의한 2D 캔버스 컨텍스트에 그린 다음 해당 이미지에 일부 텍스트를 인코딩하거나 이미지에서 텍스트를 읽으려고 시도할 수 있습니다.

이미지에서 텍스트 숨기기

이미지는 픽셀로 구성되며 각 픽셀에는 색상에 대한 정보가 포함됩니다. 예를 들어, 이미지가 RGBA 모델을 사용하여 인코딩된 경우 각 픽셀에는 해당 이미지가 나타내는 빨간색, 녹색, 파란색 및 알파(불투명도)의 양에 대한 4바이트 정보가 포함됩니다.


이미지의 일부 텍스트를 인코딩하려면 이러한 채널(예: 알파 채널) 중 하나를 사용할 수 있습니다. 이 정보는 이진 시스템(예: 01001100)으로 표시되므로 마지막 비트를 필요한 대로 전환할 수 있습니다. LSB(최하위 비트)라고 하며, 이를 변경하면 이미지 자체에 최소한의 변화가 발생하여 사람과 구별할 수 없게 됩니다.


이제 "Hello"와 같은 텍스트가 있고 이를 이미지로 인코딩한다고 가정해 보겠습니다. 이를 수행하는 알고리즘은 다음과 같습니다.


  1. "Hello" 텍스트를 바이너리로 변환합니다.


  2. 이미지 데이터의 바이트를 반복하고 해당 바이트의 LSB를 이진 텍스트의 비트로 바꿉니다(각 픽셀에는 각 색상에 대한 4바이트의 데이터가 포함되어 있습니다. 제 예에서는 이미지의 불투명도 채널을 변경하고 싶습니다). , 따라서 4번째 바이트마다 반복합니다).


  3. 디코딩할 때 언제 중지해야 하는지 알 수 있도록 메시지 끝에 널 바이트를 추가합니다.


  4. 수정된 이미지 바이트를 이미지 자체에 적용합니다.


먼저, 사용자로부터 인코딩하려는 텍스트를 가져와서 이에 대해 몇 가지 기본적인 유효성 검사를 수행해야 합니다.

 const text = document.getElementById("text").value; if (!text) { alert("Please enter some text to encode."); return; }


그런 다음 텍스트를 바이너리로 변환하고 이 텍스트를 인코딩할 이미지의 캔버스를 만들어야 합니다.

 const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; let binaryText = ""; for (let i = 0; i < text.length; i++) { let binaryChar = text.charCodeAt(i).toString(2).padStart(8, "0"); binaryText += binaryChar; }


이를 위해 간단히 각 문자를 반복하고 charCodeAt 함수를 사용하여 유니코드 인덱스를 얻을 수 있습니다. 그런 다음 이 유니코드는 바이너리로 변환되고 채워져 다른 문자와 길이가 같아집니다.


예를 들어 문자 "H"는 유니코드에서 72로 표시됩니다. 그런 다음 이 숫자를 이진수(1001000)로 변환하고 시작 부분(01001000)에 0을 추가하여 모든 문자의 길이가 동일한지 확인합니다(8비트).


그런 다음 메시지 끝에 널 바이트를 추가하여 암호를 해독할 때 실제 텍스트와 이미지의 임의 픽셀 데이터를 구별할 수 있는지 확인해야 합니다.

 binaryText += "00000000";


그런 다음 이미지가 오버플로되지 않도록 메시지를 인코딩하기에 충분한 픽셀이 있는지 확인하기 위해 몇 가지 기본 유효성 검사를 수행해야 합니다.

 if (binaryText.length > data.length / 4) { alert("Text is too long to encode in this image."); return; }


그리고 가장 흥미로운 부분인 메시지 인코딩이 나옵니다. 이전에 정의한 데이터 배열에는 이미지의 각 픽셀에 대한 RGBA 값 형식의 픽셀 정보가 포함되어 있습니다. 따라서 이미지가 RGBA로 인코딩된 경우 이미지의 각 픽셀은 데이터 배열의 4개 값으로 표시됩니다. 각 값은 해당 픽셀이 가지고 있는 빨간색, 녹색, 파란색의 양을 나타냅니다.

 for (let i = 0; i < binaryText.length; i++) { data[i * 4] = (data[i * 4] & 0b11111110) | parseInt(binaryText[i]); } ctx.putImageData(imgData, 0, 0); const outputImage = document.getElementById("outputImage"); outputImage.src = canvas.toDataURL();


위 코드에서는 바이너리로 인코딩된 텍스트를 반복합니다. data[i * 4] 수정해야 할 바이트를 찾고, 특정 채널의 바이트만 수정하려고 하므로 변수 i4 곱하여 액세스합니다.


data[i * 4] & 0b11111110 연산은 최하위 비트를 0 으로 설정합니다. 예를 들어, data[i * 4] 이진수로 10101101 이면 10101101 & 11111110 연산 결과는 10101100 됩니다. 이렇게 하면 추가 조작을 수행하기 전에 LSB가 0 으로 설정됩니다.


parseInt(binaryText[i]) 바이너리로 인코딩된 문자열의 현재 비트입니다. 1 또는 0 입니다. 그런 다음 비트별 OR( | ) 연산을 사용하여 이 비트를 LSB로 설정할 수 있습니다. 예를 들어, 비트별 OR의 왼쪽 부분이 10101100 이고 binaryText[i] 1 인 경우 10101100 | 00000001 10101101 이 됩니다. 현재 비트가 0 이면 OR의 결과는 10101100 이 됩니다. 이것이 우리가 처음에 LSB를 삭제해야 했던 이유입니다.


메시지가 인코딩되면 이를 현재 캔버스에 배치하고 canvas.toDataURL 메소드를 사용하여 HTML로 렌더링할 수 있습니다.



이미지에서 숨겨진 메시지 디코딩

이미지를 디코딩하는 과정은 실제로 인코딩보다 훨씬 간단합니다. 알파 채널만 인코딩했다는 것을 이미 알고 있으므로 4바이트마다 반복하고 마지막 비트를 읽고 이를 최종 문자열로 연결한 다음 이 데이터를 바이너리에서 유니코드 문자열로 변환할 수 있습니다.


먼저 변수를 초기화해야 합니다. imgData 는 이미 이미지 정보로 채워져 있으므로(파일 시스템에서 파일을 읽을 때마다 ctx.drawImage 호출함) 간단히 데이터 변수로 추출할 수 있습니다.


 const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imgData.data; let binaryText = ""; let decodedText = "";


그런 다음 이미지의 4번째 바이트마다 반복하고 마지막 비트를 읽고 이를 binaryText 변수에 연결해야 합니다.

 for (let i = 0; i < data.length; i += 4) { binaryText += (data[i] & 1).toString(); }


data[i] 는 인코딩된 바이트이며 LSB를 추출하기 위해 비트 AND( & ) 연산자를 사용할 수 있습니다. 두 개의 값을 취하고 해당 비트의 각 쌍에 대해 AND 연산을 수행합니다. data[i] 1 과 비교함으로써 기본적으로 픽셀 정보에서 최하위 비트를 분리하고, LSB가 1 이면 이러한 연산의 결과는 1 입니다. LSB가 0 이면 결과도 0 이 됩니다.


모든 LSB를 읽고 이를 binaryText 변수에 저장한 후에는 이를 바이너리에서 일반 텍스트로 변환해야 합니다. 각 문자가 8비트로 구성되어 있다는 것을 알고 있으므로(각 문자를 같은 길이로 만들기 위해 padStart(8, "0") 어떻게 사용했는지 기억하시나요?), binaryText 의 8번째 문자마다 반복할 수 있습니다.


그런 다음 .slice() 작업을 사용하여 반복을 기반으로 binaryText 에서 현재 바이트를 추출할 수 있습니다. parseInt(byte, 2) 함수를 사용하여 이진 문자열을 숫자로 변환할 수 있습니다. 그런 다음 결과가 0 (널 바이트)인지 확인할 수 있습니다. 변환을 중지하고 결과를 쿼리합니다. 그렇지 않으면 유니코드 숫자에 해당하는 문자를 찾아 결과 문자열에 추가할 수 있습니다.


 for (let i = 0; i < binaryText.length; i += 8) { let byte = binaryText.slice(i, i + 8); if (byte.length < 8) break; // Stop if the byte is incomplete let charCode = parseInt(byte, 2); if (charCode === 0) break; // Stop if we hit a null character decodedText += String.fromCharCode(charCode); }


그러면 디코딩된 텍스트가 사용자에게 안전하게 표시될 수 있습니다.

 document.getElementById("decodedText").textContent = decodedText; 



이 기사에 사용된 전체 코드를 내 GitHub 저장소 에 남겨 두었습니다. 자유롭게 가지고 놀아보세요. 개선할 부분이 많아요 :)

마지막 생각들

스테가노그래피는 매우 강력한 기술이며 문서 검증, 유출 방지, 이미지 AI 검증, 음악 파일 DRM 관리 등 다양한 사용 사례에 적용될 수 있습니다. 이 기술은 영상, 게임, 원시 텍스트에도 적용할 수 있어 잠재력이 크다고 생각합니다.

NFT와 블록체인 시대에는 어떻게 활용 사례를 찾아내고 이 기술이 어떻게 진화할지 지켜보는 것이 더욱 흥미롭습니다.