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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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 is_segwit_bech32_address(word)
  84. local has_upper, has_lower, has_invalid
  85. if #word > 90 then return false end
  86. fun.each(function(ch)
  87. if ch < 33 or ch > 126 then
  88. has_invalid = true
  89. elseif ch >= 97 and ch <= 122 then
  90. has_lower = true
  91. elseif ch >= 65 and ch <= 90 then
  92. has_upper = true;
  93. end
  94. end, fun.map(string.byte, fun.iter(word)))
  95. if has_invalid or (has_lower and has_upper) then
  96. return false
  97. end
  98. word = word:lower()
  99. local last_one_pos = word:find('1[^1]*$')
  100. if not last_one_pos or (last_one_pos < 1 or last_one_pos + 7 > #word) then
  101. return false
  102. end
  103. local hrp = word:sub(1, last_one_pos - 1)
  104. local d = {}
  105. local charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
  106. for i=last_one_pos + 1,#word do
  107. local c = word:sub(i, i)
  108. local pos = charset:find(c)
  109. if not pos then
  110. return false
  111. end
  112. d[#d + 1] = pos - 1
  113. end
  114. return verify_beach32_cksum(hrp, d)
  115. end
  116. rspamd_config:register_symbol{
  117. name = 'BITCOIN_ADDR',
  118. description = 'Message has a valid bitcoin wallet address',
  119. callback = function(task)
  120. local rspamd_re = require "rspamd_regexp"
  121. local btc_wallet_re = rspamd_re.create_cached('^[13LM][1-9A-Za-z]{25,34}$')
  122. local segwit_wallet_re = rspamd_re.create_cached('^[b][c]1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{14,74}$', 'i')
  123. local words_matched = {}
  124. local segwit_words_matched = {}
  125. local valid_wallets = {}
  126. for _,part in ipairs(task:get_text_parts() or {}) do
  127. local pw = part:filter_words(btc_wallet_re, 'raw', 3)
  128. if pw and #pw > 0 then
  129. for _,w in ipairs(pw) do
  130. words_matched[#words_matched + 1] = w
  131. end
  132. end
  133. pw = part:filter_words(segwit_wallet_re, 'raw', 3)
  134. if pw and #pw > 0 then
  135. for _,w in ipairs(pw) do
  136. segwit_words_matched[#segwit_words_matched + 1] = w
  137. end
  138. end
  139. end
  140. for _,word in ipairs(words_matched) do
  141. local valid = is_traditional_btc_address(word)
  142. if valid then
  143. valid_wallets[#valid_wallets + 1] = word
  144. end
  145. end
  146. for _,word in ipairs(segwit_words_matched) do
  147. local valid = is_segwit_bech32_address(word)
  148. if valid then
  149. valid_wallets[#valid_wallets + 1] = word
  150. end
  151. end
  152. if #valid_wallets > 0 then
  153. return true,1.0,valid_wallets
  154. end
  155. end,
  156. score = 0.0,
  157. group = 'scams'
  158. }