main.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import os
  2. import os.path
  3. import time
  4. import argparse
  5. import tempfile
  6. import traceback
  7. import cv2
  8. import requests
  9. from loguru import logger
  10. from Crypto.Hash import BLAKE2b
  11. try:
  12. import rdrand
  13. except ImportError:
  14. logger.warning("RdRand is not available.")
  15. class TemporaryFile:
  16. def __init__(self, name, io, delete):
  17. self.name = name
  18. self.__io = io
  19. self.__delete = delete
  20. def __getattr__(self, k):
  21. return getattr(self.__io, k)
  22. def __del__(self):
  23. if self.__delete:
  24. try:
  25. os.unlink(self.name)
  26. except FileNotFoundError:
  27. pass
  28. def NamedTemporaryFile(
  29. mode="w+b", bufsize=-1, suffix="", prefix="tmp", dir=None, delete=True
  30. ):
  31. if not dir:
  32. dir = tempfile.gettempdir()
  33. name = os.path.join(dir, prefix + os.urandom(32).hex() + suffix)
  34. if mode is None:
  35. return TemporaryFile(name, None, delete)
  36. fh = open(name, "w+b", bufsize)
  37. if mode != "w+b":
  38. fh.close()
  39. fh = open(name, mode)
  40. return TemporaryFile(name, fh, delete)
  41. def chunks(lst, n):
  42. for i in range(0, len(lst), n):
  43. yield lst[i : i + n]
  44. def run_check(cmd):
  45. logger.info(f"Executing '{cmd}'.")
  46. if os.system(cmd) != 0:
  47. raise ValueError("Exit code != 0.")
  48. def extract_image(path):
  49. logger.info(f"Extract image '{path}'.")
  50. im = cv2.imread(path)
  51. data = []
  52. rows, cols, _ = im.shape
  53. for i in range(rows):
  54. for j in range(cols):
  55. r, g, b = im[i, j]
  56. data.append(((r << 16) + (g << 8) + b) & 255)
  57. return bytes(data)
  58. def extract_wav(path):
  59. data = []
  60. with open(path, "rb") as f:
  61. for sample in chunks(f.read()[44:], 2):
  62. data.append(sample[0])
  63. return bytes(data)
  64. def extract_lsbs(data):
  65. logger.info("Extract LSBs.")
  66. buffer = []
  67. if len(data) % 2 != 0:
  68. data = data[:-1]
  69. for chunk in chunks(data, 2):
  70. tmp_byte = 0
  71. for byte in chunk:
  72. for n in range(4):
  73. tmp_byte = (tmp_byte << 1) | ((byte >> n) & 1)
  74. buffer.append(tmp_byte & 255)
  75. return bytes(buffer)
  76. def whiten(data):
  77. logger.info("Whitening.")
  78. buffer = b""
  79. for chunk in chunks(data, 128):
  80. buffer += BLAKE2b.new(data=chunk, digest_bits=256).digest()
  81. return buffer
  82. def read_video(source, duration=60):
  83. tmpf = NamedTemporaryFile(suffix=".mkv", mode=None)
  84. run_check(
  85. f"ffmpeg -hide_banner -loglevel error -y -i {source} -t {duration} -acodec copy -vcodec copy {tmpf.name}"
  86. )
  87. with tempfile.TemporaryDirectory() as tmpd:
  88. run_check(
  89. f"ffmpeg -hide_banner -loglevel error -y -i {tmpf.name} -vf mpdecimate -r 1/1 {tmpd.name}/%d.bmp"
  90. )
  91. data = b""
  92. for filename in os.listdir(tmpd.name):
  93. data += extract_image(os.path.join(tmpd.name, filename))
  94. return data
  95. def read_audio(source, duration=60):
  96. tmpf = NamedTemporaryFile(suffix=".wav", mode=None)
  97. run_check(
  98. f"ffmpeg -f alsa -i {source} -t {duration} -ar 48000 -f s16le -acodec pcm_s16le {tmpf.name}"
  99. )
  100. return extract_wav(tmpf.name)
  101. def read_audio_video(source, duration=60):
  102. tmpf = NamedTemporaryFile(suffix=".mkv", mode=None)
  103. run_check(
  104. f"ffmpeg -hide_banner -loglevel error -y -i {source} -t {duration} -acodec copy -vcodec copy {tmpf.name}"
  105. )
  106. with tempfile.TemporaryDirectory() as tmpd:
  107. run_check(
  108. f"ffmpeg -hide_banner -loglevel error -y -i {tmpf.name} -vf mpdecimate -r 1/1 {tmpd}/%d.bmp"
  109. )
  110. data_a = b""
  111. for filename in sorted(
  112. os.listdir(tmpd), key=lambda filename: int(filename.split(".")[0])
  113. ):
  114. data_a += extract_image(os.path.join(tmpd, filename))
  115. tmpf2 = NamedTemporaryFile(suffix=".wav", mode=None)
  116. run_check(
  117. f"ffmpeg -hide_banner -loglevel error -y -i {tmpf.name} -vn -ar 48000 -f s16le -acodec pcm_s16le {tmpf2.name}"
  118. )
  119. data_b = extract_wav(tmpf2.name)
  120. return bytes(a ^ b for a, b in zip(data_a, data_b))
  121. def read_rdseed(source, amount=16):
  122. data = rdrand.rdseed_get_bytes(amount)
  123. if len(data) != amount or data.count(0) == amount:
  124. raise ValueError("bad data")
  125. return data
  126. def sample(source, source_type, multiplier=1):
  127. match source_type:
  128. case "video":
  129. sampler = read_video
  130. multiplier *= 60
  131. case "audio":
  132. sampler = read_audio
  133. multiplier *= 60
  134. case "video+audio":
  135. sampler = read_audio_video
  136. multiplier *= 60
  137. case "rdseed":
  138. sampler = read_rdseed
  139. case _:
  140. raise ValueError(source_type)
  141. multiplier = int(multiplier)
  142. if multiplier < 1:
  143. raise ValueError(multiplier)
  144. logger.info("Sampling...")
  145. data = sampler(source, multiplier)
  146. logger.info(f"Sample ready: {len(data)}b.")
  147. if source_type != "rdseed":
  148. data = extract_lsbs(data)
  149. data = whiten(data)
  150. return data
  151. def push(pool_url, data, secret):
  152. resp = requests.post(
  153. f"{pool_url}/api/pool", data=data[: 1024**2], headers={"X-Secret": secret}
  154. )
  155. (logger.success if resp.status_code == 200 else logger.error)(
  156. f"{resp.status_code}: {resp.text}"
  157. )
  158. if __name__ == "__main__":
  159. parser = argparse.ArgumentParser()
  160. parser.add_argument("--source", type=str, required=True)
  161. parser.add_argument("--source-type", type=str, default="video+audio")
  162. parser.add_argument("--multiplier", type=float, default=1)
  163. parser.add_argument("--secret-file", type=str, default="./.secret")
  164. parser.add_argument("--cooldown", type=int, default=0)
  165. parser.add_argument("--pool-url", type=str, default="https://trng.iike.ru")
  166. args = parser.parse_args()
  167. with open(args.secret_file, "r") as f:
  168. lines = f.read().strip().split("\n")
  169. ident = lines[0].strip()
  170. secret = lines[1].strip()
  171. secret = f"{ident} {secret}"
  172. while True:
  173. try:
  174. data = sample(args.source, args.source_type, args.multiplier)
  175. push(args.pool_url, data, secret)
  176. except KeyboardInterrupt:
  177. logger.info("Interrupted by user.")
  178. break
  179. except Exception as e:
  180. traceback.print_exc()
  181. logger.error(e)
  182. time.sleep(args.cooldown)