You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

bitcoin.lua 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. -- Bitcoin filter rules
  14. local fun = require "fun"
  15. local bit = require "bit"
  16. local lua_util = require "lua_util"
  17. local rspamd_util = require "rspamd_util"
  18. local N = "bitcoin"
  19. local off = 0
  20. local base58_dec = fun.tomap(fun.map(
  21. function(c)
  22. off = off + 1
  23. return c, (off - 1)
  24. end,
  25. "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"))
  26. local function is_traditional_btc_address(word)
  27. local hash = require "rspamd_cryptobox_hash"
  28. local bytes = {}
  29. for i = 1, 25 do
  30. bytes[i] = 0
  31. end
  32. -- Base58 decode loop
  33. fun.each(function(ch)
  34. local acc = base58_dec[ch] or 0
  35. for i = 25, 1, -1 do
  36. acc = acc + (58 * bytes[i]);
  37. bytes[i] = acc % 256
  38. acc = math.floor(acc / 256);
  39. end
  40. end, word)
  41. -- Now create a validation tag
  42. local sha256 = hash.create_specific('sha256')
  43. for i = 1, 21 do
  44. sha256:update(string.char(bytes[i]))
  45. end
  46. sha256 = hash.create_specific('sha256', sha256:bin()):bin()
  47. -- Compare tags
  48. local valid = true
  49. for i = 1, 4 do
  50. if string.sub(sha256, i, i) ~= string.char(bytes[21 + i]) then
  51. valid = false
  52. end
  53. end
  54. return valid
  55. end
  56. -- Beach32 checksum combiner
  57. local function polymod(...)
  58. local chk = 1;
  59. local gen = { 0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3 };
  60. for _, t in ipairs({ ... }) do
  61. for _, v in ipairs(t) do
  62. local top = bit.rshift(chk, 25)
  63. chk = bit.bxor(bit.lshift(bit.band(chk, 0x1ffffff), 5), v)
  64. for i = 1, 5 do
  65. if bit.band(bit.rshift(top, i - 1), 0x1) ~= 0 then
  66. chk = bit.bxor(chk, gen[i])
  67. end
  68. end
  69. end
  70. end
  71. return chk
  72. end
  73. -- Beach32 expansion function
  74. local function hrpExpand(hrp)
  75. local ret = {}
  76. fun.each(function(byte)
  77. ret[#ret + 1] = bit.rshift(byte, 5)
  78. end, fun.map(string.byte, fun.iter(hrp)))
  79. ret[#ret + 1] = 0
  80. fun.each(function(byte)
  81. ret[#ret + 1] = bit.band(byte, 0x1f)
  82. end, fun.map(string.byte, fun.iter(hrp)))
  83. return ret
  84. end
  85. local function verify_beach32_cksum(hrp, elts)
  86. return polymod(hrpExpand(hrp), elts) == 1
  87. end
  88. local function gen_bleach32_table(input)
  89. local d = {}
  90. local i = 1
  91. local res = true
  92. local charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
  93. fun.each(function(byte)
  94. if res then
  95. local pos = charset:find(byte, 1, true)
  96. if not pos then
  97. res = false
  98. else
  99. d[i] = pos - 1
  100. i = i + 1
  101. end
  102. end
  103. end, fun.iter(input))
  104. return res and d or nil
  105. end
  106. local function is_segwit_bech32_address(task, word)
  107. local semicolon_pos = string.find(word, ':')
  108. local address_part = word
  109. if semicolon_pos then
  110. address_part = string.sub(word, semicolon_pos + 1)
  111. end
  112. local prefix = address_part:sub(1, 3)
  113. if prefix == 'bc1' or prefix:sub(1, 1) == '1' or prefix:sub(1, 1) == '3' then
  114. -- Strip beach32 prefix in bitcoin
  115. address_part = address_part:lower()
  116. local last_one_pos = address_part:find('1[^1]*$')
  117. if not last_one_pos or (last_one_pos < 1 or last_one_pos + 7 > #address_part) then
  118. return false
  119. end
  120. local hrp = address_part:sub(1, last_one_pos - 1)
  121. local addr = address_part:sub(last_one_pos + 1, -1)
  122. local decoded = gen_bleach32_table(addr)
  123. if decoded then
  124. return verify_beach32_cksum(hrp, decoded)
  125. end
  126. else
  127. -- Bitcoin cash address
  128. -- https://www.bitcoincash.org/spec/cashaddr.html
  129. local decoded = gen_bleach32_table(address_part)
  130. lua_util.debugm(N, task, 'check %s, %s decoded', word, decoded)
  131. if decoded and #decoded > 8 then
  132. if semicolon_pos then
  133. prefix = word:sub(1, semicolon_pos - 1)
  134. else
  135. prefix = 'bitcoincash'
  136. end
  137. local polymod_tbl = {}
  138. fun.each(function(byte)
  139. local b = bit.band(string.byte(byte), 0x1f)
  140. table.insert(polymod_tbl, b)
  141. end, fun.iter(prefix))
  142. -- For semicolon
  143. table.insert(polymod_tbl, 0)
  144. fun.each(function(byte)
  145. table.insert(polymod_tbl, byte)
  146. end, decoded)
  147. lua_util.debugm(N, task, 'final polymod table: %s', polymod_tbl)
  148. return rspamd_util.btc_polymod(polymod_tbl)
  149. end
  150. end
  151. end
  152. local normal_wallet_re = [[/\b[13LM][1-9A-Za-z]{25,34}\b/AL{sa_body}]]
  153. local btc_bleach_re = [[/\b(?:(?:[a-zA-Z]\w+:)|(?:bc1))?[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{14,}\b/AL{sa_body}]]
  154. config.regexp['BITCOIN_ADDR'] = {
  155. description = 'Message has a valid bitcoin wallet address',
  156. -- Use + operator to ensure that each expression is always evaluated
  157. re = string.format('(%s) + (%s) > 0', normal_wallet_re, btc_bleach_re),
  158. re_conditions = {
  159. [normal_wallet_re] = function(task, txt, s, e)
  160. local len = e - s
  161. if len <= 2 or len > 1024 then
  162. return false
  163. end
  164. local word = lua_util.str_trim(txt:sub(s + 1, e))
  165. local valid = is_traditional_btc_address(word)
  166. if valid then
  167. -- To save option
  168. task:insert_result('BITCOIN_ADDR', 1.0, word)
  169. lua_util.debugm(N, task, 'found valid traditional bitcoin addr in the word: %s',
  170. word)
  171. return true
  172. else
  173. lua_util.debugm(N, task, 'found invalid bitcoin addr in the word: %s',
  174. word)
  175. return false
  176. end
  177. end,
  178. [btc_bleach_re] = function(task, txt, s, e)
  179. local len = e - s
  180. if len <= 2 or len > 1024 then
  181. return false
  182. end
  183. local word = tostring(lua_util.str_trim(txt:sub(s + 1, e)))
  184. local valid = is_segwit_bech32_address(task, word)
  185. if valid then
  186. -- To save option
  187. task:insert_result('BITCOIN_ADDR', 1.0, word)
  188. lua_util.debugm(N, task, 'found valid bleach bitcoin addr in the word: %s',
  189. word)
  190. return true
  191. else
  192. lua_util.debugm(N, task, 'found invalid bitcoin addr in the word: %s',
  193. word)
  194. return false
  195. end
  196. end,
  197. },
  198. score = 0.0,
  199. one_shot = true,
  200. group = 'scams',
  201. }