Class: UnicodeStego

Inherits:
Object
  • Object
show all
Defined in:
lib/ccc/unicode_stego.rb

Constant Summary collapse

CHANNELS =

通道定义:每个通道包含 4 个同形字符(2 bits)

{
  apostrophe: {
    name: "撇号",
    bits: 2,
    chars: [
      { code: 0x0027, char: "'", name: "标准 ASCII 撇号" },      # 00
      { code: 0x2019, char: "\u2019", name: "右单引号" },             # 01
      { code: 0x02BC, char: "\u02BC", name: "修饰字母撇号" },         # 10
      { code: 0x02B9, char: "\u02B9", name: "修饰字母上标号" },        # 11
    ],
    regex: /['\u2019\u02BC\u02B9]/,
  },
  hyphen: {
    name: "短横线",
    bits: 2,
    chars: [
      { code: 0x002D, char: "-", name: "标准连字符" },             # 00
      { code: 0x2010, char: "\u2010", name: "连字符" },               # 01
      { code: 0x2011, char: "\u2011", name: "非断连字符" },            # 10
      { code: 0x2212, char: "\u2212", name: "减号" },                 # 11
    ],
    regex: /[-\u2010\u2011\u2212]/,
  },
  space: {
    name: "空格",
    bits: 2,
    chars: [
      { code: 0x0020, char: " ", name: "标准空格" },              # 00
      { code: 0x00A0, char: "\u00A0", name: "不间断空格" },       # 01
      { code: 0x2002, char: "\u2002", name: "En 空格" },         # 10
      { code: 0x2003, char: "\u2003", name: "Em 空格" },         # 11
    ],
    regex: /[ \u00A0\u2002\u2003]/,
  },
  period: {
    name: "句号",
    bits: 2,
    chars: [
      { code: 0x002E, char: ".", name: "标准句点" },              # 00
      { code: 0x2024, char: "\u2024", name: "一点前导" },             # 01
      { code: 0x06D4, char: "\u06D4", name: "阿拉伯句号" },           # 10
      { code: 0x3002, char: "\u3002", name: "中文句号" },             # 11
    ],
    regex: /[.\u2024\u06D4\u3002]/,
  },
  colon: {
    name: "冒号",
    bits: 2,
    chars: [
      { code: 0x003A, char: ":", name: "标准冒号" },              # 00
      { code: 0x2236, char: "\u2236", name: "比例符号" },             # 01
      { code: 0x02F8, char: "\u02F8", name: "修饰字母冒号" },         # 10
      { code: 0xA789, char: "\uA789", name: "拉丁修饰冒号" },          # 11
    ],
    regex: /[:\u2236\u02F8\uA789]/,
  },
}.freeze
CHANNEL_ORDER =

通道优先级(从高到低)

[:apostrophe, :hyphen, :space, :period, :colon].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeUnicodeStego

Returns a new instance of UnicodeStego.



77
78
79
80
# File 'lib/ccc/unicode_stego.rb', line 77

def initialize
  @channels = CHANNELS.dup
  @channel_order = CHANNEL_ORDER.dup
end

Instance Attribute Details

#channel_orderObject (readonly)

Returns the value of attribute channel_order.



75
76
77
# File 'lib/ccc/unicode_stego.rb', line 75

def channel_order
  @channel_order
end

#channelsObject (readonly)

Returns the value of attribute channels.



75
76
77
# File 'lib/ccc/unicode_stego.rb', line 75

def channels
  @channels
end

Instance Method Details

#channel_values_to_data(values) ⇒ Integer

将通道序列解码为数据

Parameters:

  • values (Array<Integer>)

    各通道的编码值序列

Returns:

  • (Integer)

    解码后的数据



150
151
152
153
154
155
156
# File 'lib/ccc/unicode_stego.rb', line 150

def channel_values_to_data(values)
  data = 0
  values.reverse_each do |v|
    data = (data << 2) | v
  end
  data
end

#count_available_slots(text) ⇒ Hash

统计文本中各通道可用字符数量

Parameters:

  • text (String)

    明文文本

Returns:

  • (Hash)

    各通道可用数量



110
111
112
113
114
115
116
117
118
119
# File 'lib/ccc/unicode_stego.rb', line 110

def count_available_slots(text)
  counts = {}
  @channel_order.each do |name|
    channel = @channels[name]
    regex = channel[:regex]
    matches = text.scan(regex)
    counts[name] = matches.length
  end
  counts
end

#data_to_channel_values(data, bits) ⇒ Array<Integer>

将数据编码为通道序列

Parameters:

  • data (Integer)

    待编码数据

  • bits (Integer)

    数据位数

Returns:

  • (Array<Integer>)

    各通道的编码值序列



137
138
139
140
141
142
143
144
145
# File 'lib/ccc/unicode_stego.rb', line 137

def data_to_channel_values(data, bits)
  values = []
  remaining = data
  (bits.to_f / 2).ceil.times do
    values << (remaining & 0b11)
    remaining >>= 2
  end
  values
end

#decode(stego_text, bits = nil) ⇒ Hash

解码:从文本中提取数据

Parameters:

  • stego_text (String)

    隐写文本

  • bits (Integer, nil) (defaults to: nil)

    数据位数(默认使用最大容量)

Returns:

  • (Hash)

    解码结果



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/ccc/unicode_stego.rb', line 211

def decode(stego_text, bits = nil)
  values = []
  used_channels = {}

  # 按优先级扫描通道
  @channel_order.each do |channel_name|
    channel = @channels[channel_name]
    regex = channel[:regex]

    stego_text.scan(regex) do |match|
      char = match.is_a?(Array) ? match[0] : match
      value = get_channel_value(channel_name, char)

      if value != -1
        values << value

        used_channels[channel_name] ||= { count: 0, values: [] }
        used_channels[channel_name][:count] += 1
        used_channels[channel_name][:values] << value
      end
    end
  end

  bits ||= values.length * 2

  # 只取需要的位数
  needed_values = (bits.to_f / 2).ceil
  actual_values = values.take(needed_values)
  data = channel_values_to_data(actual_values)

  {
    data: data,
    used_channels: used_channels,
    bits: [bits, values.length * 2].min,
    all_values: values,
  }
end

#detect(text) ⇒ Hash

检测文本是否包含隐写信息

Parameters:

  • text (String)

    待检测文本

Returns:

  • (Hash)

    检测结果



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/ccc/unicode_stego.rb', line 252

def detect(text)
  suspicious = []

  @channels.each do |name, channel|
    channel[:chars].each_with_index do |char_info, i|
      next if i == 0 # 跳过基准字符

      regex = Regexp.new(Regexp.escape(char_info[:code].chr(Encoding::UTF_8)))
      matches = text.scan(regex)
      next if matches.empty?

      suspicious << {
        channel: name,
        char: char_info[:code].chr(Encoding::UTF_8),
        code: "U+#{char_info[:code].to_s(16).upcase.rjust(4, '0')}",
        name: char_info[:name],
        count: matches.length,
      }
    end
  end

  {
    has_stego: !suspicious.empty?,
    suspicious: suspicious,
    summary: suspicious.empty? ? "未发现可疑 Unicode 字符" : "发现 #{suspicious.length} 种可疑字符,可能包含隐写信息",
  }
end

#encode(text, data, bits = nil) ⇒ Hash

编码:将数据嵌入文本

Parameters:

  • text (String)

    明文文本

  • data (Integer)

    待编码数据

  • bits (Integer, nil) (defaults to: nil)

    数据位数(默认自动计算)

Returns:

  • (Hash)

    编码结果



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/ccc/unicode_stego.rb', line 163

def encode(text, data, bits = nil)
  bits ||= (Math.log2(data).floor + 1)

  capacity = get_capacity(text)
  raise "数据需要 #{bits} bits,但文本容量只有 #{capacity} bits" if bits > capacity

  values = data_to_channel_values(data, bits)
  used_channels = {}
  value_index = 0
  result = text.dup

  # 按优先级使用通道
  @channel_order.each do |channel_name|
    break if value_index >= values.length

    channel = @channels[channel_name]
    regex = channel[:regex]
    match_count = 0

    result.gsub!(regex) do |match|
      break match if value_index >= values.length

      value = values[value_index]
      new_char = get_channel_char(channel_name, value)

      used_channels[channel_name] ||= { count: 0, values: [] }
      used_channels[channel_name][:count] += 1
      used_channels[channel_name][:values] << value

      value_index += 1
      match_count += 1
      new_char
    end
  end

  {
    stego_text: result,
    used_channels: used_channels,
    data: data,
    bits: bits,
    capacity: capacity,
  }
end

#generate_fingerprint(env = {}) ⇒ Integer

生成环境指纹(模拟 Claude Code 的用法)

Parameters:

  • env (Hash) (defaults to: {})

    环境信息

Returns:

  • (Integer)

    10-bit 环境指纹



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/ccc/unicode_stego.rb', line 297

def generate_fingerprint(env = {})
  fingerprint = 0

  # bit 0: 是否中国时区
  if env[:time_zone] == "Asia/Shanghai" || env[:time_zone] == "Asia/Urumqi"
    fingerprint |= 0b1
  end

  # bit 1-2: 代理类型
  proxy_type = 0
  proxy_type |= 0b01 if env[:is_china_domain]
  proxy_type |= 0b10 if env[:is_ai_lab]
  fingerprint |= (proxy_type & 0b11) << 1

  # bit 3: 是否使用代理
  fingerprint |= 0b1 << 3 if env[:has_proxy]

  # bit 4-5: 操作系统类型
  os_map = { "windows" => 1, "darwin" => 2, "linux" => 3 }
  os_type = os_map[env[:platform]&.downcase] || 0
  fingerprint |= (os_type & 0b11) << 4

  # bit 6-7: 语言区域
  lang_map = { "zh-cn" => 1, "en-us" => 2 }
  lang_type = lang_map[env[:locale]&.downcase] || 0
  fingerprint |= (lang_type & 0b11) << 6

  # bit 8-9: 保留/随机
  fingerprint |= (rand(4) & 0b11) << 8

  fingerprint & 0x3FF # 确保 10 bits
end

#get_capacity(text) ⇒ Integer

计算文本的最大编码容量(bits)

Parameters:

  • text (String)

    明文文本

Returns:

  • (Integer)

    最大可编码位数



124
125
126
127
128
129
130
131
# File 'lib/ccc/unicode_stego.rb', line 124

def get_capacity(text)
  counts = count_available_slots(text)
  total_bits = 0
  @channel_order.each do |name|
    total_bits += counts[name] * @channels[name][:bits]
  end
  total_bits
end

#get_channel_char(channel_name, value) ⇒ String?

获取通道的字符

Parameters:

  • channel_name (Symbol)

    通道名称

  • value (Integer)

    编码值(0-3)

Returns:

  • (String, nil)

    对应的字符



101
102
103
104
105
# File 'lib/ccc/unicode_stego.rb', line 101

def get_channel_char(channel_name, value)
  channel = @channels[channel_name]
  return nil unless channel && value >= 0 && value <= 3
  channel[:chars][value][:code].chr(Encoding::UTF_8)
end

#get_channel_value(channel_name, char) ⇒ Integer

获取通道的编码值(0-3)

Parameters:

  • channel_name (Symbol)

    通道名称

  • char (String)

    字符

Returns:

  • (Integer)

    编码值(0-3),-1 表示未找到



86
87
88
89
90
91
92
93
94
95
# File 'lib/ccc/unicode_stego.rb', line 86

def get_channel_value(channel_name, char)
  channel = @channels[channel_name]
  return -1 unless channel

  cp = char.ord
  channel[:chars].each_with_index do |c, i|
    return i if c[:code] == cp
  end
  -1
end

#parse_fingerprint(fingerprint) ⇒ Hash

解析环境指纹

Parameters:

  • fingerprint (Integer)

    10-bit 指纹

Returns:

  • (Hash)

    解析后的环境信息



333
334
335
336
337
338
339
340
341
342
# File 'lib/ccc/unicode_stego.rb', line 333

def parse_fingerprint(fingerprint)
  {
    is_china_tz: (fingerprint & 0b1) != 0,
    proxy_type: (fingerprint >> 1) & 0b11,
    has_proxy: ((fingerprint >> 3) & 0b1) != 0,
    os_type: (fingerprint >> 4) & 0b11,
    locale_type: (fingerprint >> 6) & 0b11,
    reserved: (fingerprint >> 8) & 0b11,
  }
end

#sanitize(text) ⇒ String

防御:清除文本中的隐写信息

Parameters:

  • text (String)

    待清理文本

Returns:

  • (String)

    清理后的纯文本



283
284
285
286
287
288
289
290
291
292
# File 'lib/ccc/unicode_stego.rb', line 283

def sanitize(text)
  result = text.dup
  @channels.each do |_name, channel|
    channel[:chars].each_with_index do |char_info, i|
      next if i == 0 # 跳过基准字符
      result.gsub!(char_info[:code].chr(Encoding::UTF_8), channel[:chars][0][:code].chr(Encoding::UTF_8))
    end
  end
  result
end