paint-brush
Steganography: Cách ẩn văn bản trong hình ảnh bằng JavaScripttừ tác giả@andriiromasiun
1,467 lượt đọc
1,467 lượt đọc

Steganography: Cách ẩn văn bản trong hình ảnh bằng JavaScript

từ tác giả Andrii Romasiun9m2024/06/26
Read on Terminal Reader

dài quá đọc không nổi

Steganography là một phương pháp ẩn các thông điệp bí mật trong một tập tin không bí mật khác. Người dùng có thể tải hình ảnh lên để đọc thông điệp ẩn trong đó hoặc tự mã hóa thông điệp trong hình ảnh. Bài viết này mô tả cách triển khai một công cụ mã hóa như vậy bằng JavaScript.
featured image - Steganography: Cách ẩn văn bản trong hình ảnh bằng JavaScript
Andrii Romasiun HackerNoon profile picture
0-item

Steganography là gì?

Hãy tưởng tượng bạn muốn gửi một tin nhắn bí mật cho một người bạn, nhưng kênh bạn muốn sử dụng bị xâm phạm và bị theo dõi. Bạn có thể sử dụng một số cách mã hóa, nhưng điều đó sẽ làm dấy lên sự nghi ngờ của những người đang theo dõi cuộc trò chuyện của bạn, vì vậy bạn phải sử dụng cách khác.

Ngày nay, steganography là một phương pháp giấu các tin nhắn bí mật trong một tệp không bí mật khác (như hình ảnh một con mèo), để nếu bạn gửi tệp đó thì nó sẽ không bị phát hiện. Steganography không chỉ giới hạn ở việc ẩn văn bản trong hình ảnh và thường có nghĩa là "giấu thông tin bí mật trong một tin nhắn hoặc vật thể không bí mật khác": bạn có thể ẩn một số tin nhắn trong âm thanh, video hoặc các văn bản khác bằng cách sử dụng, chẳng hạn như chuyển vị theo cột.


Steganography cũng có thể cực kỳ hữu ích trong nhiều trường hợp khác, chẳng hạn, nó có thể là một giải pháp thay thế tốt cho hình mờ trong các tài liệu nhạy cảm để bảo vệ chúng khỏi bị rò rỉ.


Có nhiều cách để ẩn thông tin trong hình ảnh, từ việc thêm văn bản vào cuối tệp đến ẩn nó trong siêu dữ liệu. Trong bài viết này, tôi muốn đề cập đến một phương pháp giấu tin tiên tiến hơn, đi xuống cấp độ nhị phân và ẩn các thông điệp trong ranh giới của chính hình ảnh.

Xây dựng một công cụ Steganographic

Giao diện người dùng

Đối với ví dụ về kỹ thuật giấu tin, tôi quyết định sử dụng JavaScript vì đây là ngôn ngữ lập trình mạnh mẽ có thể được thực thi trong trình duyệt.


Tôi đã nghĩ ra một giao diện đơn giản cho phép người dùng tải hình ảnh lên để đọc thông điệp ẩn trong đó hoặc tự mã hóa thông điệp trong hình ảnh.


 <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>


Để sử dụng nó, người dùng chỉ cần chọn một hình ảnh mà họ muốn thao tác và cố gắng giải mã một số văn bản từ nó hoặc mã hóa nó và tải hình ảnh xuống sau.



Xử lý hình ảnh

Để làm việc với hình ảnh trong JavaScript, chúng ta có thể sử dụng Canvas API . Nó cung cấp nhiều chức năng khác nhau để thao tác và vẽ hình ảnh, hoạt ảnh hoặc thậm chí đồ họa và video trò chơi.


API Canvas chủ yếu được sử dụng cho đồ họa 2D. Nếu bạn muốn làm việc với đồ họa 3D, được tăng tốc phần cứng, bạn có thể sử dụng API WebGL (ngẫu nhiên, API này cũng sử dụng phần tử <canvas>).


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


Để đọc tệp hình ảnh từ hệ thống tệp và thêm nó vào ngữ cảnh canvas, chúng ta có thể sử dụng API FileReader . Nó cho phép chúng ta dễ dàng đọc nội dung của bất kỳ tệp nào được lưu trữ trên máy tính của người dùng mà không cần thư viện tùy chỉnh.

 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]); }


Nó đọc một tệp và vẽ hình ảnh trong tệp đó vào bối cảnh canvas 2D đã xác định trước đó của chúng tôi, sau đó, chúng tôi có thể mã hóa một số văn bản vào hình ảnh đó hoặc cố gắng đọc văn bản từ hình ảnh.

Ẩn văn bản trong hình ảnh

Hình ảnh được tạo thành từ các pixel và mỗi pixel chứa thông tin về màu sắc của nó. Ví dụ: nếu một hình ảnh được mã hóa bằng mô hình RGBA, mỗi pixel sẽ chứa 4 byte thông tin về mức độ màu đỏ, lục, lam và alpha (độ mờ) mà nó thể hiện.


Để mã hóa một số văn bản trong hình ảnh, chúng ta có thể sử dụng một trong các kênh này (ví dụ: kênh alpha). Vì thông tin này được thể hiện trong hệ thống nhị phân (như 01001100), chúng ta có thể chuyển bit cuối cùng thành bất cứ thứ gì chúng ta cần. Nó được gọi là Bit ít quan trọng nhất (LSB) và việc thay đổi nó sẽ gây ra sự thay đổi tối thiểu đối với chính hình ảnh, khiến nó không thể phân biệt được với con người.


Bây giờ, hãy tưởng tượng chúng ta có văn bản như "Xin chào" và chúng ta muốn mã hóa nó thành một hình ảnh. Thuật toán để làm điều này sẽ là


  1. Chuyển đổi văn bản "Xin chào" thành nhị phân.


  2. Lặp lại qua các byte dữ liệu hình ảnh và thay thế LSB của các byte đó bằng một chút từ văn bản nhị phân (mỗi pixel chứa 4 byte dữ liệu cho mỗi màu, trong ví dụ của tôi, tôi muốn thay đổi kênh độ mờ của hình ảnh , vì vậy tôi sẽ lặp lại ở mỗi byte thứ 4).


  3. Thêm một byte null vào cuối tin nhắn để khi giải mã chúng ta biết khi nào nên dừng.


  4. Áp dụng byte hình ảnh đã sửa đổi cho chính hình ảnh đó.


Đầu tiên, chúng ta cần lấy văn bản mà chúng ta muốn mã hóa từ người dùng và thực hiện một số xác thực cơ bản trên đó.

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


Sau đó, chúng ta cần chuyển đổi văn bản thành nhị phân và tạo canvas hình ảnh mà chúng ta sẽ mã hóa văn bản này vào.

 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; }


Để làm điều này, chúng ta chỉ cần lặp lại từng ký tự và lấy chỉ mục Unicode bằng hàm charCodeAt . Unicode này sau đó được chuyển đổi thành nhị phân và được đệm để nó có cùng độ dài với bất kỳ ký tự nào khác.


Ví dụ: chữ "H" được biểu thị bằng 72 trong Unicode; sau đó chúng tôi chuyển đổi số này thành nhị phân (1001000) và thêm số 0 vào đầu (01001000) để đảm bảo rằng tất cả các chữ cái sẽ có cùng độ dài (8 bit).


Sau đó, chúng ta cần thêm một byte null vào cuối tin nhắn để đảm bảo rằng khi giải mã nó, chúng ta có thể phân biệt giữa văn bản thực và dữ liệu pixel ngẫu nhiên của hình ảnh.

 binaryText += "00000000";


Sau đó, chúng ta cần thực hiện một số xác thực cơ bản để đảm bảo rằng hình ảnh có đủ pixel để mã hóa thông điệp của chúng ta để nó không bị tràn.

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


Và sau đó đến phần thú vị nhất, mã hóa tin nhắn. Mảng dữ liệu mà chúng ta đã xác định trước đó chứa thông tin pixel ở dạng giá trị RGBA cho mỗi pixel trong ảnh. Vì vậy, nếu hình ảnh được mã hóa RGBA, mỗi pixel trong đó sẽ được biểu thị bằng 4 giá trị của mảng dữ liệu; mỗi giá trị biểu thị mức độ màu đỏ, xanh lục và xanh lam của pixel đó.

 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();


Trong đoạn mã trên, chúng tôi lặp lại văn bản được mã hóa nhị phân của mình. data[i * 4] tìm thấy một byte mà chúng ta cần sửa đổi và vì chúng ta chỉ muốn sửa đổi các byte của một kênh cụ thể nên chúng ta nhân biến i với 4 để truy cập vào nó.


Hoạt động data[i * 4] & 0b11111110 đặt bit có trọng số thấp nhất thành 0 . Ví dụ: nếu data[i * 4]10101101 ở dạng nhị phân thì thao tác 10101101 & 11111110 sẽ dẫn đến 10101100 . Điều này đảm bảo rằng LSB được đặt thành 0 trước khi chúng ta thực hiện bất kỳ thao tác nào khác với nó.


parseInt(binaryText[i]) là bit hiện tại từ chuỗi được mã hóa nhị phân; nó là 1 hoặc 0 Sau đó chúng ta có thể đặt bit này thành LSB bằng cách sử dụng thao tác OR ( | ) theo bit. Ví dụ: nếu phần bên trái của bitwise OR là 10101100binaryText[i]1 thì 10101100 | 00000001 sẽ dẫn đến 10101101 . Nếu bit hiện tại là 0 thì OR sẽ có kết quả là 10101100 . Đây là lý do tại sao chúng tôi phải xóa LSB ngay từ đầu.


Sau khi tin nhắn được mã hóa, chúng ta có thể đặt nó vào canvas hiện tại và hiển thị nó dưới dạng HTML bằng phương thức canvas.toDataURL .



Giải mã tin nhắn ẩn từ hình ảnh

Quá trình giải mã một hình ảnh thực sự đơn giản hơn nhiều so với mã hóa. Vì chúng ta đã biết rằng chúng ta chỉ mã hóa kênh alpha nên chúng ta có thể chỉ cần lặp qua mỗi byte thứ 4, đọc bit cuối cùng, nối nó thành chuỗi cuối cùng và chuyển đổi dữ liệu này từ nhị phân thành chuỗi Unicode.


Đầu tiên chúng ta cần khởi tạo các biến. Vì imgData đã được điền sẵn thông tin hình ảnh (chúng tôi gọi ctx.drawImage mỗi khi đọc tệp từ hệ thống tệp), nên chúng tôi có thể chỉ cần trích xuất nó vào biến dữ liệu.


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


Sau đó, chúng ta cần lặp lại từng byte thứ 4 của hình ảnh, đọc bit cuối cùng và nối nó với biến binaryText .

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


data[i] là byte được mã hóa và để trích xuất LSB, chúng ta có thể sử dụng toán tử AND ( & ) theo bit. Nó nhận hai giá trị và thực hiện thao tác AND trên mỗi cặp bit tương ứng. Bằng cách so sánh data[i] với 1 , về cơ bản, chúng tôi tách bit có ý nghĩa nhỏ nhất khỏi thông tin pixel và nếu LSB là 1 thì kết quả của thao tác đó là 1 . Nếu LSB bằng 0 thì kết quả cũng sẽ là 0 .


Khi chúng ta đã đọc tất cả các LSB và lưu trữ chúng trong biến binaryText , chúng ta cần chuyển đổi nó từ văn bản nhị phân sang văn bản thuần túy. Vì chúng ta biết rằng mỗi ký tự bao gồm 8 bit (hãy nhớ cách chúng ta sử dụng padStart(8, "0") để làm cho mỗi ký tự có cùng độ dài?), nên chúng ta có thể lặp lại trên mỗi ký tự thứ 8 của binaryText .


Sau đó, chúng ta có thể sử dụng thao tác .slice() để trích xuất byte hiện tại từ binaryText dựa trên lần lặp của chúng ta. Chuỗi nhị phân có thể được chuyển đổi thành số bằng hàm parseInt(byte, 2) . Sau đó, chúng tôi có thể kiểm tra xem kết quả có phải là 0 (một byte rỗng) hay không - chúng tôi dừng chuyển đổi và truy vấn kết quả. Nếu không, chúng ta có thể tìm ký tự nào tương ứng với số Unicode và thêm nó vào chuỗi kết quả của mình.


 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); }


Văn bản được giải mã sau đó có thể được hiển thị một cách an toàn cho người dùng:

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



Tôi đã để lại mã đầy đủ được sử dụng trong bài viết này trong kho GitHub của mình; hãy thoải mái chơi đùa với nó. Có rất nhiều thứ có thể được cải thiện :)

suy nghĩ cuối cùng

Steganography là một kỹ thuật rất mạnh mẽ và có thể được áp dụng cho nhiều trường hợp sử dụng khác nhau, bắt đầu từ xác minh tài liệu, ngăn chặn rò rỉ, xác minh AI hình ảnh, quản lý DRM tệp nhạc, v.v. Kỹ thuật này thậm chí có thể được áp dụng cho video, trò chơi hoặc thậm chí là văn bản thô, vì vậy tôi nghĩ nó có tiềm năng rất lớn.

Trong kỷ nguyên của NFT và chuỗi khối, điều thú vị hơn nữa là xem nó sẽ tìm ra các trường hợp sử dụng như thế nào và kỹ thuật này sẽ phát triển như thế nào.