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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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 bytes[i] = 0 end
  30. -- Base58 decode loop
  31. fun.each(function(ch)
  32. local acc = base58_dec[ch] or 0
  33. for i=25,1,-1 do
  34. acc = acc + (58 * bytes[i]);
  35. bytes[i] = acc % 256
  36. acc = math.floor(acc / 256);
  37. end
  38. end, word)
  39. -- Now create a validation tag
  40. local sha256 = hash.create_specific('sha256')
  41. for i=1,21 do
  42. sha256:update(string.char(bytes[i]))
  43. end
  44. sha256 = hash.create_specific('sha256', sha256:bin()):bin()
  45. -- Compare tags
  46. local valid = true
  47. for i=1,4 do
  48. if string.sub(sha256, i, i) ~= string.char(bytes[21 + i]) then
  49. valid = false
  50. end
  51. end
  52. return valid
  53. end
  54. -- Beach32 checksum combiner
  55. local function polymod(...)
  56. local chk = 1;
  57. local gen = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};
  58. for _,t in ipairs({...}) do
  59. for _,v in ipairs(t) do
  60. local top = bit.rshift(chk, 25)
  61. chk = bit.bxor(bit.lshift(bit.band(chk, 0x1ffffff), 5), v)
  62. for i=1,5 do
  63. if bit.band(bit.rshift(top, i - 1), 0x1) ~= 0 then
  64. chk = bit.bxor(chk, gen[i])
  65. end
  66. end
  67. end
  68. end
  69. return chk
  70. end
  71. -- Beach32 expansion function
  72. local function hrpExpand(hrp)
  73. local ret = {}
  74. fun.each(function(byte)
  75. ret[#ret + 1] = bit.rshift(byte, 5)
  76. end, fun.map(string.byte, fun.iter(hrp)))
  77. ret[#ret + 1] = 0
  78. fun.each(function(byte)
  79. ret[#ret + 1] = bit.band(byte, 0x1f)
  80. end, fun.map(string.byte, fun.iter(hrp)))
  81. return ret
  82. end
  83. local function verify_beach32_cksum(hrp, elts)
  84. return polymod(hrpExpand(hrp), elts) == 1
  85. end
  86. local function gen_bleach32_table(input)
  87. local d = {}
  88. local i = 1
  89. local res = true
  90. local charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
  91. fun.each(function(byte)
  92. if res then
  93. local pos = charset:find(byte, 1, true)
  94. if not pos then
  95. res = false
  96. else
  97. d[i] = pos - 1
  98. i = i + 1
  99. end
  100. end
  101. end, fun.iter(input))
  102. return res and d or nil
  103. end
  104. local function is_segwit_bech32_address(task, word)
  105. local semicolon_pos = string.find(word, ':')
  106. local address_part = word
  107. if semicolon_pos then
  108. address_part = string.sub(word, semicolon_pos + 1)
  109. end
  110. local prefix = address_part:sub(1, 3)
  111. if prefix == 'bc1' or prefix:sub(1, 1) == '1' or prefix:sub(1, 1) == '3' then
  112. -- Strip beach32 prefix in bitcoin
  113. address_part = address_part:lower()
  114. local last_one_pos = address_part:find('1[^1]*$')
  115. if not last_one_pos or (last_one_pos < 1 or last_one_pos + 7 > #address_part) then
  116. return false
  117. end
  118. local hrp = address_part:sub(1, last_one_pos - 1)
  119. local addr = address_part:sub(last_one_pos + 1, -1)
  120. local decoded = gen_bleach32_table(addr)
  121. if decoded then
  122. return verify_beach32_cksum(hrp, decoded)
  123. end
  124. else
  125. -- Bitcoin cash address
  126. -- https://www.bitcoincash.org/spec/cashaddr.html
  127. local decoded = gen_bleach32_table(address_part)
  128. lua_util.debugm(N, task, 'check %s, %s decoded', word, decoded)
  129. if decoded and #decoded > 8 then
  130. if semicolon_pos then
  131. prefix = word:sub(1, semicolon_pos - 1)
  132. else
  133. prefix = 'bitcoincash'
  134. end
  135. local polymod_tbl = {}
  136. fun.each(function(byte)
  137. local b = bit.band(string.byte(byte), 0x1f)
  138. table.insert(polymod_tbl, b)
  139. end, fun.iter(prefix))
  140. -- For semicolon
  141. table.insert(polymod_tbl, 0)
  142. fun.each(function(byte) table.insert(polymod_tbl, byte) end, decoded)
  143. lua_util.debugm(N, task, 'final polymod table: %s', polymod_tbl)
  144. return rspamd_util.btc_polymod(polymod_tbl)
  145. end
  146. end
  147. end
  148. local normal_wallet_re = [[/\b[13LM][1-9A-Za-z]{25,34}\b/AL{sa_body}]]
  149. local btc_bleach_re = [[/\b(?:(?:[a-zA-Z]\w+:)|(?:bc1))?[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{14,}\b/AL{sa_body}]]
  150. config.regexp['BITCOIN_ADDR'] = {
  151. description = 'Message has a valid bitcoin wallet address',
  152. -- Use + operator to ensure that each expression is always evaluated
  153. re = string.format('(%s) + (%s) > 0', normal_wallet_re, btc_bleach_re),
  154. re_conditions = {
  155. [normal_wallet_re] = function(task, txt, s, e)
  156. local len = e - s
  157. if len <= 2 or len > 1024 then
  158. return false
  159. end
  160. local word = lua_util.str_trim(txt:sub(s + 1, e))
  161. local valid = is_traditional_btc_address(word)
  162. if valid then
  163. -- To save option
  164. task:insert_result('BITCOIN_ADDR', 1.0, word)
  165. lua_util.debugm(N, task, 'found valid traditional bitcoin addr in the word: %s',
  166. word)
  167. return true
  168. else
  169. lua_util.debugm(N, task, 'found invalid bitcoin addr in the word: %s',
  170. word)
  171. return false
  172. end
  173. end,
  174. [btc_bleach_re] = function(task, txt, s, e)
  175. local len = e - s
  176. if len <= 2 or len > 1024 then
  177. return false
  178. end
  179. local word = tostring(lua_util.str_trim(txt:sub(s + 1, e)))
  180. local valid = is_segwit_bech32_address(task, word)
  181. if valid then
  182. -- To save option
  183. task:insert_result('BITCOIN_ADDR', 1.0, word)
  184. lua_util.debugm(N, task, 'found valid bleach bitcoin addr in the word: %s',
  185. word)
  186. return true
  187. else
  188. lua_util.debugm(N, task, 'found invalid bitcoin addr in the word: %s',
  189. word)
  190. return false
  191. end
  192. end,
  193. },
  194. score = 0.0,
  195. one_shot = true,
  196. group = 'scams',
  197. }