2019-06-18 13:46:34 +02:00
|
|
|
--[[
|
2022-03-25 21:16:35 +01:00
|
|
|
Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
|
2019-06-18 13:46:34 +02:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
]]--
|
|
|
|
|
|
|
|
-- Bitcoin filter rules
|
|
|
|
|
|
|
|
local fun = require "fun"
|
2019-10-23 16:53:27 +02:00
|
|
|
local bit = require "bit"
|
2020-07-28 17:34:20 +02:00
|
|
|
local lua_util = require "lua_util"
|
2020-07-28 18:13:32 +02:00
|
|
|
local rspamd_util = require "rspamd_util"
|
2020-07-28 17:34:20 +02:00
|
|
|
local N = "bitcoin"
|
|
|
|
|
2019-06-18 13:46:34 +02:00
|
|
|
local off = 0
|
|
|
|
local base58_dec = fun.tomap(fun.map(
|
|
|
|
function(c)
|
|
|
|
off = off + 1
|
|
|
|
return c,(off - 1)
|
|
|
|
end,
|
|
|
|
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"))
|
|
|
|
|
2019-10-23 16:53:27 +02:00
|
|
|
local function is_traditional_btc_address(word)
|
|
|
|
local hash = require "rspamd_cryptobox_hash"
|
|
|
|
|
|
|
|
local bytes = {}
|
|
|
|
for i=1,25 do bytes[i] = 0 end
|
|
|
|
-- Base58 decode loop
|
|
|
|
fun.each(function(ch)
|
|
|
|
local acc = base58_dec[ch] or 0
|
|
|
|
for i=25,1,-1 do
|
|
|
|
acc = acc + (58 * bytes[i]);
|
|
|
|
bytes[i] = acc % 256
|
|
|
|
acc = math.floor(acc / 256);
|
|
|
|
end
|
|
|
|
end, word)
|
|
|
|
-- Now create a validation tag
|
|
|
|
local sha256 = hash.create_specific('sha256')
|
|
|
|
for i=1,21 do
|
|
|
|
sha256:update(string.char(bytes[i]))
|
|
|
|
end
|
|
|
|
sha256 = hash.create_specific('sha256', sha256:bin()):bin()
|
|
|
|
|
|
|
|
-- Compare tags
|
|
|
|
local valid = true
|
|
|
|
for i=1,4 do
|
|
|
|
if string.sub(sha256, i, i) ~= string.char(bytes[21 + i]) then
|
|
|
|
valid = false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return valid
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Beach32 checksum combiner
|
|
|
|
local function polymod(...)
|
|
|
|
local chk = 1;
|
|
|
|
local gen = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};
|
|
|
|
for _,t in ipairs({...}) do
|
|
|
|
for _,v in ipairs(t) do
|
|
|
|
local top = bit.rshift(chk, 25)
|
|
|
|
|
|
|
|
chk = bit.bxor(bit.lshift(bit.band(chk, 0x1ffffff), 5), v)
|
|
|
|
for i=1,5 do
|
|
|
|
if bit.band(bit.rshift(top, i - 1), 0x1) ~= 0 then
|
|
|
|
chk = bit.bxor(chk, gen[i])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return chk
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Beach32 expansion function
|
|
|
|
local function hrpExpand(hrp)
|
|
|
|
local ret = {}
|
|
|
|
fun.each(function(byte)
|
|
|
|
ret[#ret + 1] = bit.rshift(byte, 5)
|
|
|
|
end, fun.map(string.byte, fun.iter(hrp)))
|
|
|
|
ret[#ret + 1] = 0
|
|
|
|
fun.each(function(byte)
|
|
|
|
ret[#ret + 1] = bit.band(byte, 0x1f)
|
|
|
|
end, fun.map(string.byte, fun.iter(hrp)))
|
|
|
|
|
|
|
|
return ret
|
|
|
|
end
|
|
|
|
|
|
|
|
local function verify_beach32_cksum(hrp, elts)
|
|
|
|
return polymod(hrpExpand(hrp), elts) == 1
|
|
|
|
end
|
|
|
|
|
2020-07-30 14:09:28 +02:00
|
|
|
|
2020-06-19 18:19:59 +02:00
|
|
|
local function gen_bleach32_table(input)
|
|
|
|
local d = {}
|
|
|
|
local i = 1
|
|
|
|
local res = true
|
|
|
|
local charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
|
2019-10-23 16:53:27 +02:00
|
|
|
|
2020-06-19 18:19:59 +02:00
|
|
|
fun.each(function(byte)
|
|
|
|
if res then
|
2020-07-30 14:09:28 +02:00
|
|
|
local pos = charset:find(byte, 1, true)
|
2020-06-19 18:19:59 +02:00
|
|
|
if not pos then
|
|
|
|
res = false
|
|
|
|
else
|
|
|
|
d[i] = pos - 1
|
|
|
|
i = i + 1
|
|
|
|
end
|
2019-10-23 16:53:27 +02:00
|
|
|
end
|
2020-06-19 18:19:59 +02:00
|
|
|
end, fun.iter(input))
|
2019-10-23 16:53:27 +02:00
|
|
|
|
2020-06-19 18:19:59 +02:00
|
|
|
return res and d or nil
|
|
|
|
end
|
2019-10-23 16:53:27 +02:00
|
|
|
|
2020-07-28 18:13:32 +02:00
|
|
|
local function is_segwit_bech32_address(task, word)
|
2020-06-21 21:49:38 +02:00
|
|
|
local semicolon_pos = string.find(word, ':')
|
2020-07-22 22:33:22 +02:00
|
|
|
local address_part = word
|
2020-06-21 21:49:38 +02:00
|
|
|
if semicolon_pos then
|
2020-07-22 22:33:22 +02:00
|
|
|
address_part = string.sub(word, semicolon_pos + 1)
|
2020-06-21 21:49:38 +02:00
|
|
|
end
|
|
|
|
|
2020-07-22 22:33:22 +02:00
|
|
|
local prefix = address_part:sub(1, 3)
|
2019-10-23 16:53:27 +02:00
|
|
|
|
2020-06-19 22:27:08 +02:00
|
|
|
if prefix == 'bc1' or prefix:sub(1, 1) == '1' or prefix:sub(1, 1) == '3' then
|
2020-06-19 18:19:59 +02:00
|
|
|
-- Strip beach32 prefix in bitcoin
|
2020-07-22 22:33:22 +02:00
|
|
|
address_part = address_part:lower()
|
|
|
|
local last_one_pos = address_part:find('1[^1]*$')
|
|
|
|
if not last_one_pos or (last_one_pos < 1 or last_one_pos + 7 > #address_part) then
|
2019-10-23 16:53:27 +02:00
|
|
|
return false
|
|
|
|
end
|
2020-07-22 22:33:22 +02:00
|
|
|
local hrp = address_part:sub(1, last_one_pos - 1)
|
|
|
|
local addr = address_part:sub(last_one_pos + 1, -1)
|
2020-06-19 18:19:59 +02:00
|
|
|
local decoded = gen_bleach32_table(addr)
|
2019-10-23 16:53:27 +02:00
|
|
|
|
2020-06-19 18:19:59 +02:00
|
|
|
if decoded then
|
|
|
|
return verify_beach32_cksum(hrp, decoded)
|
|
|
|
end
|
2020-07-28 21:52:58 +02:00
|
|
|
else
|
2020-07-22 22:33:22 +02:00
|
|
|
-- Bitcoin cash address
|
|
|
|
-- https://www.bitcoincash.org/spec/cashaddr.html
|
|
|
|
local decoded = gen_bleach32_table(address_part)
|
2020-07-28 18:13:32 +02:00
|
|
|
lua_util.debugm(N, task, 'check %s, %s decoded', word, decoded)
|
2020-07-22 22:33:22 +02:00
|
|
|
|
|
|
|
if decoded and #decoded > 8 then
|
2020-07-28 21:52:58 +02:00
|
|
|
if semicolon_pos then
|
|
|
|
prefix = word:sub(1, semicolon_pos - 1)
|
|
|
|
else
|
|
|
|
prefix = 'bitcoincash'
|
|
|
|
end
|
|
|
|
|
2020-07-28 18:13:32 +02:00
|
|
|
local polymod_tbl = {}
|
2020-07-22 22:33:22 +02:00
|
|
|
fun.each(function(byte)
|
2020-07-28 18:13:32 +02:00
|
|
|
local b = bit.band(string.byte(byte), 0x1f)
|
|
|
|
table.insert(polymod_tbl, b)
|
2020-07-22 22:33:22 +02:00
|
|
|
end, fun.iter(prefix))
|
|
|
|
|
|
|
|
-- For semicolon
|
2020-07-28 18:13:32 +02:00
|
|
|
table.insert(polymod_tbl, 0)
|
2020-07-22 22:33:22 +02:00
|
|
|
|
2020-07-28 21:24:14 +02:00
|
|
|
fun.each(function(byte) table.insert(polymod_tbl, byte) end, decoded)
|
2020-07-28 18:13:32 +02:00
|
|
|
lua_util.debugm(N, task, 'final polymod table: %s', polymod_tbl)
|
2020-07-22 22:33:22 +02:00
|
|
|
|
2020-07-28 18:13:32 +02:00
|
|
|
return rspamd_util.btc_polymod(polymod_tbl)
|
2020-06-19 22:27:08 +02:00
|
|
|
end
|
2020-06-19 18:19:59 +02:00
|
|
|
end
|
2019-10-23 16:53:27 +02:00
|
|
|
end
|
|
|
|
|
2020-07-28 18:13:32 +02:00
|
|
|
local normal_wallet_re = [[/\b[13LM][1-9A-Za-z]{25,34}\b/AL{sa_body}]]
|
2021-03-30 17:44:32 +02:00
|
|
|
local btc_bleach_re = [[/\b(?:(?:[a-zA-Z]\w+:)|(?:bc1))?[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{14,}\b/AL{sa_body}]]
|
2019-10-23 16:53:27 +02:00
|
|
|
|
2020-07-28 17:34:20 +02:00
|
|
|
config.regexp['BITCOIN_ADDR'] = {
|
2019-06-27 18:17:04 +02:00
|
|
|
description = 'Message has a valid bitcoin wallet address',
|
2021-06-23 11:26:40 +02:00
|
|
|
-- Use + operator to ensure that each expression is always evaluated
|
|
|
|
re = string.format('(%s) + (%s) > 0', normal_wallet_re, btc_bleach_re),
|
2020-07-28 17:34:20 +02:00
|
|
|
re_conditions = {
|
|
|
|
[normal_wallet_re] = function(task, txt, s, e)
|
2021-09-17 10:46:32 +02:00
|
|
|
local len = e - s
|
|
|
|
if len <= 2 or len > 1024 then
|
2020-07-28 17:34:20 +02:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2020-07-30 14:13:26 +02:00
|
|
|
local word = lua_util.str_trim(txt:sub(s + 1, e))
|
2020-07-28 17:34:20 +02:00
|
|
|
local valid = is_traditional_btc_address(word)
|
|
|
|
|
|
|
|
if valid then
|
|
|
|
-- To save option
|
|
|
|
task:insert_result('BITCOIN_ADDR', 1.0, word)
|
2020-07-28 18:13:32 +02:00
|
|
|
lua_util.debugm(N, task, 'found valid traditional bitcoin addr in the word: %s',
|
2020-07-28 17:34:20 +02:00
|
|
|
word)
|
|
|
|
return true
|
|
|
|
else
|
|
|
|
lua_util.debugm(N, task, 'found invalid bitcoin addr in the word: %s',
|
|
|
|
word)
|
|
|
|
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
[btc_bleach_re] = function(task, txt, s, e)
|
2021-09-17 10:46:32 +02:00
|
|
|
local len = e - s
|
|
|
|
if len <= 2 or len > 1024 then
|
2020-07-28 18:13:32 +02:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2020-07-30 14:13:26 +02:00
|
|
|
local word = tostring(lua_util.str_trim(txt:sub(s + 1, e)))
|
2020-07-28 18:13:32 +02:00
|
|
|
local valid = is_segwit_bech32_address(task, word)
|
|
|
|
|
|
|
|
if valid then
|
|
|
|
-- To save option
|
|
|
|
task:insert_result('BITCOIN_ADDR', 1.0, word)
|
|
|
|
lua_util.debugm(N, task, 'found valid bleach bitcoin addr in the word: %s',
|
|
|
|
word)
|
|
|
|
return true
|
|
|
|
else
|
|
|
|
lua_util.debugm(N, task, 'found invalid bitcoin addr in the word: %s',
|
|
|
|
word)
|
|
|
|
|
|
|
|
return false
|
|
|
|
end
|
2020-07-28 17:34:20 +02:00
|
|
|
end,
|
|
|
|
},
|
2019-06-18 13:46:34 +02:00
|
|
|
score = 0.0,
|
2020-07-28 17:34:20 +02:00
|
|
|
one_shot = true,
|
|
|
|
group = 'scams',
|
2021-06-23 11:26:40 +02:00
|
|
|
}
|