- 實驗設計要求:
模塊:
- 用戶認證:數字簽名方案
- 信息加密:公鑰加密方案
- 信息隱藏:LSB方案
要求:
實現登陸驗證,信息發送和接收(文本和圖片),信息加密解密,信息簽名和簽名驗證
- 關鍵代碼解釋說明:
1.數字簽名:使用java security包中的keypairgenerator 生成rsa祕鑰對,長度爲512:
2.獲取祕鑰對中的publicexponent,modulus,privateexponent:
3.加密代碼:
其中,輸入要加密的信息後,轉成byte[],通過取模運算,產生加密信息,轉成字節。
4.解密代碼:
同解密過程一樣。
5.數字簽名驗證:
通過登錄用戶輸入的姓名,在數據庫中查找到對應的e和n(即公鑰),通過用戶輸入的私鑰d,對“yaxin”用私鑰加密後,服務端用公鑰進行解密,內容爲yaxin即爲驗證成功。
6.數據庫查找代碼:
Resultset集存儲客戶的e,n,d 返回arraylist可供查找。
8.客戶註冊代碼:
若客戶爲新註冊用戶,選擇註冊按鈕,生成新的祕鑰對,用戶自行保留私鑰d,新生成的用戶信息將被存儲在數據庫chatroom1中:
7.客戶登陸代碼:
客戶需要輸入暱稱和私鑰d進行數字簽名登陸驗證。
8.客戶私聊消息加密:通過被傳輸消息的客戶在數據庫系統中存儲的公鑰,傳輸消息方將數據進行加密,被傳輸消息方先將數據解密,在顯示出來,客戶端只會看到解密的消息,非被傳輸方會看到加密後的信息,但是無法解出。
三、代碼展示:
Server類:
package MyChatRoom;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.ihep.Mysqllink;
import com.ihep.RSADemo;
public class Server {
private ServerSocket sc;
private ExecutorService threadpool;
private List<PrintWriter> pws;
private Map<String,PrintWriter> maps;
public Server() {
try {
sc= new ServerSocket(8088);
} catch (IOException e) {
e.printStackTrace();
}
threadpool
= Executors.newFixedThreadPool(100);
pws = new ArrayList<PrintWriter>();
maps = new HashMap<String,PrintWriter>();
RSADemo rsa=new RSADemo();
Mysqllink msq=new Mysqllink();
//msq.Mysqllinkstart();
}
public void start() {
int count = 0;
while(true) {
System.out.println("wait..");
try {
Socket s= sc.accept();
ClientHandle hc
= new ClientHandle(s);
threadpool.execute(hc);
} catch (IOException e) {
e.printStackTrace();
}
count++;
System.out.println
("第"+count+"個客戶連接成功!");
}
}
//自定義方法
//遍歷map集合,將所有在線的好友name拼接成字符串,
//並返回
public String findAll(){
synchronized (maps) {
//1.遍歷map集合,拿到所有在線人的name
Set<String> names = maps.keySet();
//2. 遍歷set集合,拿到所有人的name
String nms="";
Iterator<String> it = names.iterator();
while(it.hasNext()){
String nm = it.next();
nms +=nm;//重新賦值 拼接每一個在線好友的name
nms +=",";
}
return nms;
}
}
//創建ClientHandle類(助教模板類)
class ClientHandle implements Runnable{
Socket s;
PrintWriter pw;
String name;
public ClientHandle(Socket s) {
this.s=s;
}
public void run() {
//獲取該客戶端的輸入流,讀取該客戶端的消息
try {
InputStream is
= s.getInputStream();
InputStreamReader isr
= new InputStreamReader(is);
BufferedReader br
= new BufferedReader(isr);
//從socket中獲取該客戶端的輸出流
OutputStream os = s.getOutputStream();
OutputStreamWriter osw
= new OutputStreamWriter(os);
pw
= new PrintWriter(osw,true);
//在讀消息之前,先讀客戶端發來的 暱稱
name = br.readLine();
//將name和name對應的pw存入map集合中
synchronized (maps) {
maps.put(name,pw);
}
pw.println("歡迎你,"
+name+",當前在線人數【"+(pws.size()+1)+"】人!");
//服務器告訴剛上線的客戶端都有哪些人在線
String nms = findAll();
//至此,所有在線的name都在nms字符串中
pw.println("當前在線好友:"+nms);
sendToAll(name+
"上線了!當前在線人數【"
+(pws.size()+1)+"】人!");
sendToAll("當前在線好友:"+nms);
//將該客戶端的輸出流加入到集合中存儲起來
synchronized (pws) {
pws.add(pw);
}
SimpleDateFormat sdf
= new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date d = null;
while(true) {
String str = br.readLine();//阻塞
d = new Date(); //當前時間
String time = sdf.format(d);
System.out.println(name+":"+str);//輸出在控制檯
if(str.startsWith("@")){
//單聊
//1. 解析出要單聊的name,也就是截取@和:之間的name
String to_name = str.substring(1, str.indexOf(":"));
//2. 根據第一步解析出的人名從map中獲取到
// 該人名所對應的pw
PrintWriter to_pw = maps.get(to_name);
//3. 解析出單聊的消息,也就是截取:之後的內容
String msg = str.substring(str.indexOf(":")+1);
Mysqllink msq=new Mysqllink();
ArrayList<String> list=msq.selectsj(to_name);
//System.out.println(list.get(0).toString()+'\n'+list.get(1).toString()+'\n'+list.get(2).toString());
RSADemo rsa=new RSADemo();
byte[] mesag=rsa.encrypt((msg.trim()).getBytes(), list.get(0).toString(), list.get(1).toString());
System.out.println("加密過後的消息:"+mesag);
byte[] msg_afterde=rsa.decrypt(mesag, list.get(2).toString(), list.get(1).toString());
String msg_afterde1=new String(msg_afterde);
System.out.println("解密後的消息"+msg_afterde1);
//4. 使用第2步獲得的pw將第3步解析出的msg進行發送
to_pw.println(time+"\n"+name+"悄悄對你說:"+msg_afterde1);
//5. 消息也要返回給自己,不僅是給要單聊的那個人
pw.println(time+"\n"+"我對"+to_name+"悄悄說:"+msg_afterde1);
sendToAll(time+"\n"+name+":"+mesag.toString());
}else{
//羣發
sendToAll(time+"\n"+name+":"+str);
}
}
}catch(Exception e) {
//追蹤一異常信息
//e.printStackTrace();
}finally{
//不管出不出異常,finally都會被執行
//而且是最後執行
synchronized (pws) {
pws.remove(pw);
}
// 當好友下線,除了從list集合中移除該客戶端的pw
// 也要從maps集合中移除該客戶端信息
synchronized (maps) {
maps.remove(name);
}
//移除該客戶端之後,再向所有人廣播,**下線了
sendToAll(name+"下線了!當前人數【"+pws.size()+"】人!");
//廣播 更新好友列表:
//1.可以調用sendtoall (遍歷list中的所有pw)
//2.也可以遍歷map集合中的所有values
String nms = findAll();
sendToAll("當前在線好友:"+nms);
if(s!=null){
try {
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
//把廣播功能(羣發)for循環抽取出來
public void sendToAll(String msg){
synchronized (pws) {
for (int i = 0; i <pws.size(); i++) {
pws.get(i).println(msg);
}
}
}
public static void main(String[] args) {
Server s = new Server();
s.start();
}
}
Client類:
package MyChatRoom;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.security.*;
import java.security.interfaces.*;
import java.math.*;
import java.awt.BorderLayout;
import java.sql.ResultSet;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Scanner;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import com.ihep.Mysqllink;
import com.ihep.RSADemo;
public class Client implements ActionListener{
private Socket s;
private JFrame f; //聊天界面窗口
private JTextArea jta;//顯示聊天內容的文本域
private JLabel lab ; //顯示暱稱的標籤
private JTextField jtf;//輸入消息的框
private JButton btn;//發送消息的按鈕
private JButton btn1;
private JFrame dl1;
private JButton b1;
private JButton b2;
private java.awt.Graphics g;
private int rgb;
private String name; //客戶端輸入的暱稱
private PrintWriter pw;
private String siyao;
public void setname(String n) {
name=n;
}
public Client() {
Mysqllink mysql=new Mysqllink();
btn1=new JButton("圖片");
dl1=new JFrame("登錄/註冊界面");
JPanel jp=new JPanel();
b1=new JButton("登錄");
b2=new JButton("註冊");
//對界面組件(零部件)進行初始化
f = new JFrame();
jta = new JTextArea(16,16); //行 列
jta.setEditable(false);//不可編輯
jta.setFont(
new Font("微軟雅黑",Font.ITALIC,16)
);
jtf = new JTextField(16); //每行字數
btn = new JButton("發送");
//讓用戶在輸入框中輸入要連接的服務器的ip和port
String ip =
JOptionPane.showInputDialog("請輸入服務器IP:");
String p =
JOptionPane.showInputDialog("請輸入端口:");
name = JOptionPane.showInputDialog("請輸入暱稱:");
//把字符串端口轉換成int數字端口
initdl();
showdl();
b1.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dl1.dispose();
//name
// = JOptionPane.showInputDialog("請輸入暱稱:");
//setname(name);
System.out.println(name);
siyao
= JOptionPane.showInputDialog("請輸入私鑰d");
System.out.println(name);
try {
ArrayList<String> list1=mysql.selectsj(name);
RSADemo rsa=new RSADemo();
rsa.yanzheng(siyao,list1.get(1).toString(),list1.get(0).toString());
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
init();
show();
}
});
b2.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dl1.dispose();
//name = JOptionPane.showInputDialog("請輸入暱稱:");
//setname(name);
System.out.println(name);
RSADemo rsa=new RSADemo();
rsa.generateKey();
// System.out.println(rsa.gete());
try {
mysql.addsj(name, rsa.gete(),rsa.getn(),rsa.getd());
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println("你的私鑰d"+rsa.getd());
System.out.println("你的公鑰"+rsa.gete());
init();
show();
//start();
//System.out.println(name);
}
});
lab = new JLabel(name); //待改動
try {
int port = Integer.parseInt(p);
s = new Socket(ip,port);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
btn.addActionListener(this);//點擊按鈕將消息發出
jtf.addActionListener(this);//按回車消息就發出
}
//自定義方法,將各個組件放到窗口的指定位置
public void init(){
//將顯示聊天內容的文本域添加至可滾動畫布上
JScrollPane jsp = new JScrollPane(jta);
//將標籤 輸入框 發送按鈕 添加到一個普通畫布上
JPanel p = new JPanel();
p.add(lab);
p.add(jtf);
p.add(btn);
p.add(btn1);
JPanel jp3=new JPanel();
p.add(jp3);
//將上述2個畫布添加到窗口frame上
f.add(jsp,BorderLayout.CENTER);
f.add(p,BorderLayout.SOUTH);
this.g=jp3.getGraphics();
}
//自定義方法,顯示窗口
public void show(){
f.pack();// 自適應調整大小
f.setLocationRelativeTo(null);//居中
f.setDefaultCloseOperation(3);//真正關閉
f.setVisible(true); //顯示出來
}
public void initdl() {
JPanel jp=new JPanel();
jp.add(b2);
jp.add(b1);
dl1.add(jp,BorderLayout.CENTER);
}
public void showdl() {
dl1.pack();
dl1.setLocationRelativeTo(null);//居中
dl1.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
dl1.setVisible(true);
//System.out.println(name);
}
public void start() {
readMsg();
try {
OutputStream os
= s.getOutputStream();
OutputStreamWriter osw
= new OutputStreamWriter(os);
pw
= new PrintWriter(osw,true);
pw.println(name);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//自定義方法,創建匿名內部類線程,
//一直監聽服務器有發來消息
public void readMsg() {
new Thread() {
public void run() {
//從socket中獲取輸入流,不停讀取
try {
InputStream is
= s.getInputStream();
InputStreamReader isr
= new InputStreamReader(is);
BufferedReader br
= new BufferedReader(isr);
String str = null;
while((str=br.readLine())!=null) {
//System.out.println(str);
jta.append(str+"\n");
}
}catch(Exception e) {
}
}
}.start();
}
public static void main(String[] args) {
Client c = new Client();
c.start();
}
//當按鈕等組件被點擊,該方法能自動執行
public void actionPerformed(ActionEvent e) {
//1. 獲取到輸入框中的信息
String msg = jtf.getText();
//2. 使用客戶端的輸出流將信息寫出
pw.println(msg);
//3. 清空輸入框
jtf.setText("");
}
}
數據庫代碼:
package com.ihep;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.math.*;
public class Mysqllink {
Connection con = null;
public Mysqllink() {
//驅動程序名
String driver = "com.mysql.cj.jdbc.Driver";
//URL指向要訪問的數據庫名mydata
String url = "jdbc:mysql://localhost:3306/wyx?serverTimezone=UTC";
//"jdbc:mysql://xx.x.xx.xxx:3306/health?useUnicode=true&characterEncoding=UTF8"/>
//MySQL配置時的用戶名
String user = "root";
//MySQL配置時的密碼
String password = "Wyx10230331";
//遍歷查詢結果集
//加載驅動程序
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//1.getConnection()方法,連接MySQL數據庫!!
try {
con = DriverManager.getConnection(url,user,password);
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
if(!con.isClosed())
System.out.println("Succeeded connecting to the Database!");
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void addsj(String name,String e,String n,String d) throws SQLException {
//
PreparedStatement psql;
//ResultSet res;
psql = con.prepareStatement("insert into chatroom1(暱稱,e,n,d ) values(?,?,?,?)");
psql.setString(1, name); //設置參數1,創建id爲3212的數據
psql.setString(2, e);
psql.setString(3, n);
psql.setString(4, d);
psql.executeUpdate(); //執行更新
con.close();
psql.close();
}
//public static void main(String[] args) {
// Mysqllink mysql=new Mysqllink();
//}
public ArrayList<String> selectsj(String n) throws SQLException {
Statement statement = con.createStatement();
//要執行的SQL語句
//3.ResultSet類,用來存放獲取的結果集!!
String id=new String();
System.out.println(n);
String name=String.valueOf(n);
String sql="select *from chatroom1 where 暱稱='"+name+"'";
ResultSet rs = statement.executeQuery(sql);
System.out.println(name);
ArrayList<String> list=new ArrayList<String>();
// System.out.println(rs.next());
while(rs.next()){
// System.out.println(rs.getString(1));
// System.out.println(rs.getString(2));
list.add(rs.getString("e"));//0
list.add(rs.getString("n"));//1
list.add(rs.getString("d"));//2
//System.out.println("數據庫裏的e "+list.get(0)+'\n'+"數據庫裏的n "+list.get(1)+'\n'+"數據庫裏的d "+rs.getString("d")+'\n'+rs.getString("暱稱"));
}
rs.close();
con.close();
return list;
}
}
public class CopyDemo {
public static void main(String[] args) throws Exception {
//讀取原文件
RandomAccessFile raf1 = new RandomAccessFile("6fc2d95f1837905a0ee88cb5e1da58fa.jpg", "r");
//將讀取到的字節寫入目標文件
RandomAccessFile raf2 = new RandomAccessFile("6fc2d95f1837905a0ee88cb5e1da58fa_copy.jpg", "rw");
//注意:讀到文件末尾返回-1
//d 表示讀到的字節
int d = -1;
System.out.println("開始複製");
//獲取自1970年1月1日0時到現在的毫秒數
long time1=System.currentTimeMillis();
while((d=raf1.read())!=-1) {
raf2.write(d);
}
long time2 = System.currentTimeMillis();
System.out.println("用時: "+(time2-time1)+"ms");
raf1.close();
raf2.close();
}
}
信息隱藏:LSB方案實現:
package TEST;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.plaf.FontUIResource;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.Enumeration;
public class InfoHideToBmpImage extends JFrame {
private int displayWidth, displayHeight;
private JTextArea textArea;
private InfoHideToBmpImage() {
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
displayWidth = (int) (screenSize.getWidth() * 0.7);
displayHeight = (int) (screenSize.getHeight() * 0.7);
initFrame();
}
private void initFrame() {
Util.setUIFont(new FontUIResource("微軟雅黑", 0, 20));
setLayout(new BorderLayout());
setJMenuBar(initMenuBar());
add(initTextAreaJScrollPane(), BorderLayout.SOUTH);
setTitle("LSB隱藏器");
setSize(displayWidth, displayHeight);
//設置窗體出現在屏幕中間
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
private final String FILE_MENU_NAME = "File";
private final String OPEN_MENU_ITEM_NAME = "Open";
private final String SAVE_MENU_ITEM_NAME = "Save";
private final String LSB_MENU_NAME = "LSB";
private final String HIDE_MENU_ITEM_NAME = "Hide";
private final String SHOW_MENU_ITEM_NAME = "Show";
private final String BMP = "bmp";
private JMenuBar initMenuBar() {
JMenuBar menuBar = new JMenuBar();
MenuItemListener listener = new MenuItemListener();
JMenu menu = new JMenu(FILE_MENU_NAME);
JMenuItem openItem = new JMenuItem(OPEN_MENU_ITEM_NAME);
openItem.addActionListener(listener);
JMenuItem saveItem = new JMenuItem(SAVE_MENU_ITEM_NAME);
saveItem.addActionListener(listener);
menu.add(openItem);
menu.add(saveItem);
menuBar.add(menu);
menu = new JMenu(LSB_MENU_NAME);
JMenuItem hideItem = new JMenuItem(HIDE_MENU_ITEM_NAME);
hideItem.addActionListener(listener);
JMenuItem showItem = new JMenuItem(SHOW_MENU_ITEM_NAME);
showItem.addActionListener(listener);
menu.add(hideItem);
menu.add(showItem);
menuBar.add(menu);
return menuBar;
}
class MenuItemListener implements ActionListener {
private final String DEFAULT_CURRENT_DIRECTORY_PATH = "C:/Users/LW/Desktop";
private JFileChooser fileChooser;
private BmpImageInfo imageInfo;
private static final byte OPEN_FILE = 1;
private static final byte SAVE_FILE = 2;
MenuItemListener() {
fileChooser = new JFileChooser(DEFAULT_CURRENT_DIRECTORY_PATH);
fileChooser.setFileFilter(new FileNameExtensionFilter("24位位圖(*.bmp)", BMP));
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
}
@Override
public void actionPerformed(ActionEvent e) {
switch (e.getActionCommand()) {
case OPEN_MENU_ITEM_NAME: {
openHandler();
break;
}
case SAVE_MENU_ITEM_NAME: {
saveHandler();
break;
}
case HIDE_MENU_ITEM_NAME: {
hideHandler();
break;
}
case SHOW_MENU_ITEM_NAME: {
showHandler();
break;
}
default: {
break;
}
}
}
private void showHandler() {
if (null == imageInfo) {
textArea.setText("請選擇圖片後重試");
return;
}
String info = Util.analysisHideInfo(imageInfo);
if ("".equals(info.trim())) {
textArea.setText("提示信息: 解析出來的信息爲空");
return;
}
textArea.setText(info);
}
private void hideHandler() {
if (imageInfo == null || imageInfo.blue == null) {
textArea.setText("請打開bmp圖片文件後重試!");
return;
}
String text = textArea.getText();
if (null == text || text.trim().length() <= 0) {
textArea.setText("請在文本框中輸入需要隱藏的文本信息後重試");
return;
}
if (Util.hideInfoToBmp(imageInfo, textArea.getText())) {
textArea.setText("隱藏文本信息成功");
return;
}
textArea.setText("隱藏文本信息失敗");
}
private void openHandler() {
File file = selectFile(OPEN_FILE);
if (null == file) {
return;
}
imageInfo = Util.analysisBmpImage(file);
showBmpImage(imageInfo);
textArea.setText("打開圖片文件成功!");
if (imageInfo.getBfOffBits() > BmpImageInfo.DEFAULT_BF_OFF_BITS) {
textArea.append(" 圖片文件有可能存在隱藏信息");
}
}
private void saveHandler() {
File file = new File(generateSaveFileName());
fileChooser.setSelectedFile(file);
file = selectFile(SAVE_FILE);
Util.saveBmpImage(imageInfo, file);
textArea.setText("保存圖片文件成功");
}
private String generateSaveFileName() {
File selectedFile = fileChooser.getSelectedFile();
String name = selectedFile.getName();
name = name.substring(name.lastIndexOf('_') + 1);
String parent = selectedFile.getParent();
String now = LocalTime.now().toString().replaceAll(":", "-");
return String.join("", parent, "/", now, "_", name);
}
private File selectFile(byte type) {
// 不是確認
if (type == OPEN_FILE && JFileChooser.APPROVE_OPTION != fileChooser.showOpenDialog(null)) {
return null;
}
if (type == SAVE_FILE && JFileChooser.APPROVE_OPTION != fileChooser.showSaveDialog(null)) {
return null;
}
File selectedFile = fileChooser.getSelectedFile();
String fileName = selectedFile.getName();
String suffixName = fileName.substring(fileName.lastIndexOf(".") + 1);
if (!BMP.equals(suffixName)) {
System.out.println("非BMP類型文件");
return null;
}
return selectedFile;
}
}
private JScrollPane initTextAreaJScrollPane() {
JTextArea textArea = new JTextArea();
this.textArea = textArea;
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
JScrollPane scrollPane = new JScrollPane(textArea);
scrollPane.setPreferredSize(new Dimension(displayWidth, 200));
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
return scrollPane;
}
private void showBmpImage(BmpImageInfo imageInfo) {
DrawPanel drawPanel = new DrawPanel(imageInfo);
JScrollPane scrollPane = new JScrollPane();
scrollPane.setViewportView(drawPanel);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
add(scrollPane);
// 刷新界面
setVisible(true);
}
/**
* 繪圖面板
*/
class DrawPanel extends JPanel {
BmpImageInfo bmpImageInfo;
DrawPanel(BmpImageInfo imageInfo) {
bmpImageInfo = imageInfo;
// 將畫板撐大,這樣當圖片長寬過大時 jScrollPane 纔會顯示滾動條
setPreferredSize(new Dimension(imageInfo.getImageWidth(), imageInfo.getImageHeight()));
}
@Override
public void paint(Graphics g) {
super.paint(g);
if (null == bmpImageInfo) {
return;
}
int height = bmpImageInfo.getImageHeight();
int width = bmpImageInfo.getImageWidth();
// 水平居中x 初始座標
int initX = 0;
if (displayWidth > width) {
Point location = getLocation();
initX = (int) location.getX() + ((displayWidth - width) >> 1);
}
// 這裏只考慮了 height 爲正數的情況
for (int h = 0; h < height; h++) {
for (int w = 0; w < width; w++) {
g.setColor(new Color(bmpImageInfo.red[h][w], bmpImageInfo.green[h][w], bmpImageInfo.blue[h][w]));
g.fillRect(w + initX, h, 1, 1);
}
}
}
}
public static void main(String[] args) {
new InfoHideToBmpImage();
}
}
class BmpImageInfo {
/**
* 默認偏移量
*/
static final int DEFAULT_BF_OFF_BITS = 54;
private int height = -1;
private int width = -1;
byte[] fileHeader = new byte[14];
byte[] informationHeader = new byte[40];
/**
* 位圖數據數組,即一個像素的三個分量的數據數組
*/
int[][] red, green, blue;
/**
* 類型是否是位圖
*
* @return true/false
*/
boolean isBmpType() {
return fileHeader[0] == 0X42 && fileHeader[1] == 0X4D;
}
/**
* 是否是24位bmp
*
* @return true : 是;false: 否
*/
boolean is24BitCount() {
return informationHeader[15] == 0 && informationHeader[14] == 24;
}
/**
* 初始化位圖數據矩陣
*
* @return true:成功;false:失敗
*/
boolean initBitmapDataArr() {
int imageWidth = getImageWidth();
int imageHeight = getImageHeight();
boolean flag = imageHeight != 0 && imageWidth != 0;
if (flag) {
red = new int[imageHeight][imageWidth];
green = new int[imageHeight][imageWidth];
blue = new int[imageHeight][imageWidth];
}
return flag;
}
/**
* 獲取偏移量
*
* @return 偏移量
*/
int getBfOffBits() {
return bytesToInt(Arrays.copyOfRange(fileHeader, 10, 14));
}
/**
* 設置偏移量
*
* @param offset 偏移量
*/
void setBfOffBits(int offset) {
if (offset <= DEFAULT_BF_OFF_BITS) {
return;
}
byte[] bs = intToBytes(offset);
int index = 10, bi = 0;
while (index < fileHeader.length) {
fileHeader[index] = bs[bi++];
index++;
}
}
int getImageWidth() {
if (width == -1) {
width = bytesToInt(Arrays.copyOfRange(informationHeader, 4, 8));
}
return width;
}
int getImageHeight() {
// 需要注意的是 height 有可能爲負數
if (height == -1) {
height = bytesToInt(Arrays.copyOfRange(informationHeader, 8, 12));
}
return height;
}
private int bytesToInt(byte[] bytes) {
if (null == bytes || bytes.length <= 0) {
return 0;
}
return (bytes[3] & 0xff << 24) | (bytes[2] & 0xff) << 16 | (bytes[1] & 0xff) << 8 | bytes[0] & 0xff;
}
private byte[] intToBytes(int integer) {
byte[] bytes = new byte[4];
bytes[3] = (byte) (integer >> 24);
bytes[2] = (byte) (integer >> 16);
bytes[1] = (byte) (integer >> 8);
bytes[0] = (byte) (integer);
return bytes;
}
}
class Util {
private Util() {
}
/**
* 設置全局頁面字體大小
*
* @param f FontUIResource實例
*/
static void setUIFont(FontUIResource f) {
Enumeration<Object> keys = UIManager.getDefaults().keys();
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
if (UIManager.get(key) instanceof FontUIResource) {
UIManager.put(key, f);
}
}
}
private static final String END_BINARY_STR = "0101010101010101";
private static final char VALUE_0 = '0';
static boolean hideInfoToBmp(BmpImageInfo imageInfo, String text) {
imageInfo.setBfOffBits(imageInfo.getBfOffBits() + 1);
String binary = toBinary(text).concat(END_BINARY_STR);
char[] chars = binary.toCharArray();
int i = 0, j = 0, cnt = 0, len = chars.length;
for (; i < imageInfo.getImageHeight(); i++) {
for (; j < imageInfo.getImageWidth(); j++) {
imageInfo.blue[i][j] = getModifyBitData(imageInfo.blue[i][j], chars[cnt++]);
if (cnt >= len) {
return true;
}
imageInfo.green[i][j] = getModifyBitData(imageInfo.green[i][j], chars[cnt++]);
if (cnt >= len) {
return true;
}
imageInfo.red[i][j] = getModifyBitData(imageInfo.red[i][j], chars[cnt++]);
if (cnt >= len) {
return true;
}
}
}
imageInfo.setBfOffBits(BmpImageInfo.DEFAULT_BF_OFF_BITS);
return false;
}
private static int getModifyBitData(int oldValue, char c) {
// 0 ASCLL 48
if (c == VALUE_0) {
return oldValue & 0XE;
}
return oldValue | 0X1;
}
static String analysisHideInfo(BmpImageInfo imageInfo) {
if (imageInfo.getBfOffBits() == BmpImageInfo.DEFAULT_BF_OFF_BITS) {
return "";
}
StringBuilder binStrBuilder = new StringBuilder(imageInfo.blue[0].length << 3);
StringBuilder wordBin = new StringBuilder(16);
int cnt = 0;
flag:
for (int i = 0; i < imageInfo.getImageHeight(); i++) {
for (int j = 0; j < imageInfo.getImageWidth(); j++) {
wordBin.append(imageInfo.blue[i][j] & 0X1);
if (++cnt >= 16) {
if (isEndWithAppend(binStrBuilder, wordBin)) {
break flag;
}
cnt = 0;
}
wordBin.append(imageInfo.green[i][j] & 0X1);
if (++cnt >= 16) {
if (isEndWithAppend(binStrBuilder, wordBin)) {
break flag;
}
cnt = 0;
}
wordBin.append(imageInfo.red[i][j] & 0X1);
if (++cnt >= 16) {
if (isEndWithAppend(binStrBuilder, wordBin)) {
break flag;
}
cnt = 0;
}
}
}
return toString(binStrBuilder.toString());
}
/**
* 添加內容,並判斷是否結束
*
* @param binStrBuilder 二進制串
* @param wordBin 一個字的二進制串
* @return true:結束;false:否
*/
private static boolean isEndWithAppend(StringBuilder binStrBuilder, StringBuilder wordBin) {
if (END_BINARY_STR.equals(wordBin.toString())) {
return true;
}
binStrBuilder.append(wordBin);
wordBin.delete(0, 16);
return false;
}
/**
* 解析bmp圖片
*/
static BmpImageInfo analysisBmpImage(File file) {
BmpImageInfo imageInfo = new BmpImageInfo();
if (!isWindowOperatingSystem()) {
System.out.println("非windows操作系統,終止");
return imageInfo;
}
if (null == file) {
return imageInfo;
}
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
bis.read(imageInfo.fileHeader);
if (!imageInfo.isBmpType()) {
System.out.println("非bmp類型文件");
return imageInfo;
}
bis.read(imageInfo.informationHeader);
if (!imageInfo.is24BitCount()) {
System.out.println("非24位bmp類型文件");
return imageInfo;
}
if (imageInfo.getBfOffBits() > BmpImageInfo.DEFAULT_BF_OFF_BITS) {
System.out.println("bmp文件偏移超過默認偏移量,可能存在隱藏信息");
bis.skip(imageInfo.getBfOffBits() - BmpImageInfo.DEFAULT_BF_OFF_BITS);
}
if (!imageInfo.initBitmapDataArr()) {
System.out.println("初始化位圖數據矩陣失敗,可能未讀取信息頭數據");
return imageInfo;
}
int imageWidth = imageInfo.getImageWidth();
// 通過計算得到每行計算機需要填充的字符數。
// 爲什麼要填充?這是因爲windows系統在掃描數據的時候,每行都是按照4個字節的倍數來讀取的。
// 因爲圖片是由每個像素點組成。而每個像素點都是由3個顏色分量來構成的,而每個分量佔據1個字節。
// 因此在內存存儲中實際圖片數據每行的長度是width*3
int skipCount = getSkipCount(imageWidth);
int imageHeight = imageInfo.getImageHeight();
// 這裏需要注意,因爲根據bmp的保存格式。
// 位圖數據中height的值如果是正數的話:
// 那麼數據就是按從下到上,從左到右的順序來保存。這個稱之爲倒向位圖。
// 反之就是按從上到下,從左到右的順序來保存。這個則稱之爲正向位圖。
if (imageHeight < 0) {
// todo 大多數BMP位圖是倒向的位圖,imageHeight是正值,所以負值的情況暫未實現
return null;
}
for (int i = imageHeight - 1; i >= 0; i--) {
for (int j = 0; j < imageWidth; j++) {
// 這裏遍歷的時候,一定要注意本來像素是有RGB來表示,
// 但是在存儲的時候由於windows是小段存儲,所以在內存中是BGR順序。
imageInfo.blue[i][j] = bis.read();
imageInfo.green[i][j] = bis.read();
imageInfo.red[i][j] = bis.read();
}
// 跳過每一行的補0項 ,網上有些是在j==0時進行跳過,表示傻傻不明白
bis.skip(skipCount);
}
} catch (IOException e) {
e.printStackTrace();
}
return imageInfo;
}
/**
* 獲取填充的補0數
*
* @param width 圖像像素寬度
* @return 跳過數目
*/
private static int getSkipCount(int width) {
int skipCount = 0;
int m = width * 3 % 4;
if (m != 0) {
skipCount = 4 - m;
}
return skipCount;
}
/**
* 保存bmp圖片
*
* @param imageInfo BmpImageInfo 實例
* @param file 保存文件
*/
static void saveBmpImage(BmpImageInfo imageInfo, File file) {
if (null == imageInfo || null == file) {
return;
}
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file))) {
bos.write(imageInfo.fileHeader);
bos.write(imageInfo.informationHeader);
int bfOffBits = imageInfo.getBfOffBits();
// 寫入多餘的偏移量
if (bfOffBits > BmpImageInfo.DEFAULT_BF_OFF_BITS) {
bos.write(new byte[bfOffBits - BmpImageInfo.DEFAULT_BF_OFF_BITS]);
}
int height = imageInfo.getImageHeight();
int width = imageInfo.getImageWidth();
int skipCount = getSkipCount(width);
for (int h = height - 1; h >= 0; h--) {
for (int w = 0; w < width; w++) {
int blue = imageInfo.blue[h][w];
int green = imageInfo.green[h][w];
int red = imageInfo.red[h][w];
bos.write(blue);
bos.write(green);
bos.write(red);
}
if (skipCount > 0) {
bos.write(new byte[skipCount]);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static boolean isWindowOperatingSystem() {
String osName = System.getProperty("os.name");
return osName.startsWith("Window");
}
private static String[] PREFIX_POOL = {"0", "00", "000", "0000", "00000", "000000", "0000000", "00000000",
"000000000", "0000000000", "00000000000", "000000000000",
"0000000000000", "00000000000000", "000000000000000"
};
/**
* 字符串轉換成二進制流
*
* @param str 字符串
* @return 二進制流
*/
private static String toBinary(String str) {
if (null == str || str.trim().length() == 0) {
return null;
}
char[] strChar = str.toCharArray();
StringBuilder result = new StringBuilder(strChar.length << 4);
for (int i = 0; i < strChar.length; i++) {
// 二進制ASCII碼
String s = Integer.toBinaryString(strChar[i]);
// 補0
if (s.length() < 16) {
s = PREFIX_POOL[15 - s.length()].concat(s);
}
result.append(s);
}
return result.toString();
}
/**
* 二進制流轉換成字符串
*
* @param binaryString 二進制流
* @return 字符串
*/
private static String toString(String binaryString) {
if (null == binaryString || binaryString.trim().length() == 0) {
return null;
}
int len = binaryString.length();
StringBuilder sb = new StringBuilder(len >> 4);
int step = 16;
for (int i = 0; i < len; i += step) {
// 將ascll碼強轉成字符
sb.append((char) (int) Integer.parseInt(binaryString.substring(i, i + step), 2));
}
return sb.toString();
}
}
附:操作過程,成果展示。