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 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. --[[
  2. Copyright (c) 2019, Vsevolod Stakhov <vsevolod@highsecure.ru>
  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 off = 0
  17. local base58_dec = fun.tomap(fun.map(
  18. function(c)
  19. off = off + 1
  20. return c,(off - 1)
  21. end,
  22. "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"))
  23. local function is_traditional_btc_address(word)
  24. local hash = require "rspamd_cryptobox_hash"
  25. local bytes = {}
  26. for i=1,25 do bytes[i] = 0 end
  27. -- Base58 decode loop
  28. fun.each(function(ch)
  29. local acc = base58_dec[ch] or 0
  30. for i=25,1,-1 do
  31. acc = acc + (58 * bytes[i]);
  32. bytes[i] = acc % 256
  33. acc = math.floor(acc / 256);
  34. end
  35. end, word)
  36. -- Now create a validation tag
  37. local sha256 = hash.create_specific('sha256')
  38. for i=1,21 do
  39. sha256:update(string.char(bytes[i]))
  40. end
  41. sha256 = hash.create_specific('sha256', sha256:bin()):bin()
  42. -- Compare tags
  43. local valid = true
  44. for i=1,4 do
  45. if string.sub(sha256, i, i) ~= string.char(bytes[21 + i]) then
  46. valid = false
  47. end
  48. end
  49. return valid
  50. end
  51. -- Beach32 checksum combiner
  52. local function polymod(...)
  53. local chk = 1;
  54. local gen = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};
  55. for _,t in ipairs({...}) do
  56. for _,v in ipairs(t) do
  57. local top = bit.rshift(chk, 25)
  58. chk = bit.bxor(bit.lshift(bit.band(chk, 0x1ffffff), 5), v)
  59. for i=1,5 do
  60. if bit.band(bit.rshift(top, i - 1), 0x1) ~= 0 then
  61. chk = bit.bxor(chk, gen[i])
  62. end
  63. end
  64. end
  65. end
  66. return chk
  67. end
  68. -- Beach32 expansion function
  69. local function hrpExpand(hrp)
  70. local ret = {}
  71. fun.each(function(byte)
  72. ret[#ret + 1] = bit.rshift(byte, 5)
  73. end, fun.map(string.byte, fun.iter(hrp)))
  74. ret[#ret + 1] = 0
  75. fun.each(function(byte)
  76. ret[#ret + 1] = bit.band(byte, 0x1f)
  77. end, fun.map(string.byte, fun.iter(hrp)))
  78. return ret
  79. end
  80. local function verify_beach32_cksum(hrp, elts)
  81. return polymod(hrpExpand(hrp), elts) == 1
  82. end
  83. local function gen_bleach32_table(input)
  84. local d = {}
  85. local i = 1
  86. local res = true
  87. local charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
  88. fun.each(function(byte)
  89. if res then
  90. local pos = charset:find(byte)
  91. if not pos then
  92. res = false
  93. else
  94. d[i] = pos - 1
  95. i = i + 1
  96. end
  97. end
  98. end, fun.iter(input))
  99. return res and d or nil
  100. end
  101. local function is_segwit_bech32_address(word)
  102. local semicolon_pos = string.find(word, ':')
  103. if semicolon_pos then
  104. word = string.sub(word, semicolon_pos + 1)
  105. end
  106. local prefix = word:sub(1, 3)
  107. if prefix == 'bc1' or prefix:sub(1, 1) == '1' or prefix:sub(1, 1) == '3' then
  108. -- Strip beach32 prefix in bitcoin
  109. word = word:lower()
  110. local last_one_pos = word:find('1[^1]*$')
  111. if not last_one_pos or (last_one_pos < 1 or last_one_pos + 7 > #word) then
  112. return false
  113. end
  114. local hrp = word:sub(1, last_one_pos - 1)
  115. local addr = word:sub(last_one_pos + 1, -1)
  116. local decoded = gen_bleach32_table(addr)
  117. if decoded then
  118. return verify_beach32_cksum(hrp, decoded)
  119. end
  120. else
  121. -- BCH address
  122. -- 1 byte address type (who cares)
  123. -- XXX bytes address hash (who cares)
  124. -- 40 bit checksum
  125. local rspamd_util = require 'rspamd_util'
  126. local decoded = rspamd_util.decode_base32(word:lower(), 'bleach')
  127. if decoded then
  128. local bytes = decoded:bytes()
  129. -- The version byte’s most signficant bit is reserved and must be 0.
  130. -- The 4 next bits indicate the type of address and the 3 least significant bits indicate the size of the hash.
  131. local version = bit.band(bytes[1], 128)
  132. local addr_type = bit.rshift(bit.band(bytes[1], 120), 3)
  133. local _ = bit.band(bytes[1], 7) -- hash size
  134. if version == 0 and (addr_type == 0 or addr_type == 8)then
  135. -- TODO: Add checksum validation some day
  136. return true
  137. end
  138. end
  139. end
  140. end
  141. rspamd_config:register_symbol{
  142. name = 'BITCOIN_ADDR',
  143. description = 'Message has a valid bitcoin wallet address',
  144. callback = function(task)
  145. local rspamd_re = require "rspamd_regexp"
  146. local btc_wallet_re = rspamd_re.create_cached('^[13LM][1-9A-Za-z]{25,34}$')
  147. local segwit_wallet_re = rspamd_re.create_cached('^(?:bc1|[13]|(?:[^:]*:))?[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{14,}$', 'i')
  148. local words_matched = {}
  149. local segwit_words_matched = {}
  150. local valid_wallets = {}
  151. for _,part in ipairs(task:get_text_parts() or {}) do
  152. local pw = part:filter_words(btc_wallet_re, 'raw', 3)
  153. if pw and #pw > 0 then
  154. for _,w in ipairs(pw) do
  155. words_matched[#words_matched + 1] = w
  156. end
  157. end
  158. pw = part:filter_words(segwit_wallet_re, 'raw', 3)
  159. if pw and #pw > 0 then
  160. for _,w in ipairs(pw) do
  161. segwit_words_matched[#segwit_words_matched + 1] = w
  162. end
  163. end
  164. end
  165. for _,word in ipairs(words_matched) do
  166. local valid = is_traditional_btc_address(word)
  167. if valid then
  168. valid_wallets[#valid_wallets + 1] = word
  169. end
  170. end
  171. for _,word in ipairs(segwit_words_matched) do
  172. local valid = is_segwit_bech32_address(word)
  173. if valid then
  174. valid_wallets[#valid_wallets + 1] = word
  175. end
  176. end
  177. if #valid_wallets > 0 then
  178. return true,1.0,valid_wallets
  179. end
  180. end,
  181. score = 0.0,
  182. group = 'scams'
  183. }