/ proof-sh / proof.sh
proof.sh
  1  #!/usr/bin/env bash
  2  set -o errexit -o nounset -o pipefail
  3  
  4  # TODO: what about openssl versions? maybe use python for signing?
  5  # TODO: tell user how to install utilities
  6  
  7  # This script does the following:
  8  # 1. Ask user for her GitHub username and Ethereum address (eth_addr)
  9  # 2. Negotiate with user which SSH key to use
 10  # 3. Find at least one match of key and encrypted_key that decrypts succesfully
 11  # 4. Decrypt encrypted_key to tmp_eth_key
 12  # 5. Sign sender ethereum address: sign[tmp_eth_key](eth_addr)
 13  # 6. Encode signature (#5) and merkle proof and output result
 14  
 15  # keys.bin format (CSV):
 16  # GH UserName,Encrypted[userId,tmp_eth_addr,tmp_eth_key,merkle proof]
 17  
 18  trap 'echo GOT IT ; exit 0' SIGTERM
 19  
 20  # check_program_in_path "program"
 21  check_program_in_path() {
 22    program="${1}"
 23    if ! type -p "${program}" &>/dev/null; then
 24        printf '%s\n' "error: ${program} is not installed."
 25        printf '%s\n' "You should run install script first"
 26        printf '%s\n' "or use your package manager to install it."
 27        exit 1
 28    fi
 29  }
 30  
 31  # while true; do :; done
 32  
 33  # check that everything installed
 34  PATH="./bin:${PATH}"
 35  for i in age base64 sha3sum; do
 36    check_program_in_path $i
 37  done
 38  
 39  SSH_KEYS_DIR="$HOME/.ssh"
 40  
 41  ask_ssh_key() {
 42      SSH_KEYS=()
 43      # list all files from ~/.ssh, except for *.pub, known_hosts, config and log files (tmux sometimes puts logs there)
 44      while IFS= read -r -d $'\0'; do
 45          SSH_KEYS+=("$REPLY")
 46      done < <(find "$SSH_KEYS_DIR" -mindepth 1 -maxdepth 1 ! -name "*.pub" ! -name "known_hosts*" ! -name "config" ! -name "*.log" -print0)
 47  
 48      select fname in "${SSH_KEYS[@]}"; do
 49          echo "$fname"
 50          break
 51      done
 52  }
 53  
 54  WORK_DIR="$(pwd)/__sh_cache__"
 55  DECRYPTED_DATA="$WORK_DIR/decrypted.data"
 56  ETH_KEY_DER="$WORK_DIR/tmp_eth.key.der"
 57  ETH_KEY="$WORK_DIR/tmp_eth.key"
 58  OPENSSL_STDERR="$WORK_DIR/openssl.stderr"
 59  AGE_STDERR="$WORK_DIR/age.stderr"
 60  
 61  mkdir -p $WORK_DIR
 62  
 63  METADATA_BIN="metadata.bin"
 64  # $# is the number of arguments
 65  if [ $# -gt 1 ]; then
 66      GITHUB_USERNAME="$1"
 67      ETHEREUM_ADDRESS="$2"
 68  else
 69      if [ ! -f "$METADATA_BIN" ]; then
 70          echo "$METADATA_BIN doesn't exist"
 71          exit 1
 72      fi
 73  
 74      printf "\nWelcome to the proof generation script for Fluence Developer Reward Airdrop."
 75      printf "\n5%% of the FLT supply is allocated to ~110,000 developers who contributed into open source web3 repositories during last year."
 76      printf "\nPublic keys of selected Github accounts were added into a smart contract on Ethereum. Claim your allocation and help us build the decentralized internet together!"
 77      printf "\n"
 78      printf "\nCheck if you are eligible and proceed with claiming"
 79  
 80      read -r -p "Enter your github username so we can check if you are participating in the airdrop: " GITHUB_USERNAME
 81  
 82      printf "\nEthereum wallet address is necessary to generate a proof that you will send through our web page."
 83      printf "\n\033[33mImportant notice: you need to make a claim transaction from the entered address!\033[0m\n\n"
 84  
 85      read -r -p "Enter the ethereum address to which you plan to receive the airdrop: " ETHEREUM_ADDRESS
 86  
 87      STR_LENGTH=$(echo "$ETHEREUM_ADDRESS" | sed -e 's/^0x//' | awk '{ print length }')
 88      if [ "$STR_LENGTH" -ne 40 ]; then
 89          echo "$ETHEREUM_ADDRESS is not an Ethereum address. Must be of 40 or 42 (with 0x) characters, was $STR_LENGTH chars"
 90          exit 1
 91      fi
 92      NON_HEX_BYTES_LENGTH=$(echo "$ETHEREUM_ADDRESS" | sed -e 's/^0x//' | tr -d "[:xdigit:]" | awk '{ print length }')
 93      if [ "$NON_HEX_BYTES_LENGTH" -ne 0 ]; then
 94          echo "$ETHEREUM_ADDRESS is not an Ethereum address. Must be hexadecimal, has non-hexadecimal symbols."
 95          exit 1
 96      fi
 97  fi
 98  
 99  KEY_ARG_PATH=''
100  if [ $# -gt 2 ]; then
101      KEY_ARG_PATH="$3"
102  fi
103  
104  ENCRYPTED_KEYS=()
105  while IFS='' read -r line; do ENCRYPTED_KEYS+=("$line"); done < <(grep -i "^${GITHUB_USERNAME}," "${METADATA_BIN}" || true)
106  
107  # ${#ENCRYPTED_KEYS[@]} -- calculates number of elements in the array
108  if [ ${#ENCRYPTED_KEYS[@]} -gt 1 ]; then
109      echo "Found ${#ENCRYPTED_KEYS[@]} encrypted keys for your GitHub username. That means you have several SSH keys published on GitHub"
110  #    echo "Any of your keys would work. We have encrypted a temporary keypair for each of your SSH keys."
111  elif [ ${#ENCRYPTED_KEYS[@]} -gt 0 ]; then
112      echo "Found an encrypted key for your GitHub username"
113  else
114      echo "This'$GITHUB_USERNAME' Github account is not eligible for claiming"
115      exit 1
116  fi
117  
118  printf "\n\tNOTE: your SSH key is used ONLY LOCALLY to decrypt a message and generate Token Claim Proof."
119  printf "\n\tScript will explicitly ask your consent before using the key."
120  printf "\n\tIf you have any technical issues, take a look at the following logs:\n\t\t$OPENSSL_STDERR\n\t\t$AGE_STDERR\n\tReport any issues to https://fluence.chat \n\n"
121  
122  printf "Now the script needs your ssh key to generate proof. \n"
123  
124  while true; do
125      if [ -n "$KEY_ARG_PATH" ] && [ -f "$KEY_ARG_PATH" ]; then
126          KEY_PATH=$KEY_ARG_PATH
127      else
128          if [ -d "$SSH_KEYS_DIR" ]; then
129              # shellcheck disable=SC2162 # user can have spaces in the path to ssh key and use backslashes to escape them
130              read -p "Enter path to the private SSH key to use or just press Enter to show existing keys: " KEY_PATH
131              if [ -z "$KEY_PATH" ]; then
132                  KEY_PATH=$(ask_ssh_key)
133              fi
134          else
135              # shellcheck disable=SC2162 # user can have spaces in the path to ssh key and use backslashes to escape them
136              read -p "Enter path to the private SSH key to use: " KEY_PATH
137              if [ -z "$KEY_PATH" ]; then
138                  continue
139              fi
140          fi
141  
142          if ! [ -f "$KEY_PATH" ]; then
143              echo "Specified $KEY_PATH  file does not exits or not a SSH private key"
144              continue
145          fi
146  
147          read -p "Will use SSH key to generate proof data. Press enter to proceed. "
148          printf "\n"
149      fi
150  
151      rm -f "$DECRYPTED_DATA"
152      printf "\n"
153  
154      for encrypted in "${ENCRYPTED_KEYS[@]}"; do
155          # contains encrypted (user_id, eth_tmp_key, merkle proof)
156          ENCRYPTED_DATA=$(echo "$encrypted" | cut -d',' -f2)
157  
158          set +o errexit
159          echo "$ENCRYPTED_DATA" | xxd -r -p -c 1000 | age --decrypt --identity "$KEY_PATH" --output "$DECRYPTED_DATA" 2>$AGE_STDERR
160          exit_code=$?
161          set -o errexit
162  
163          if [ $exit_code -ne 0 ]; then
164              continue
165          else
166              break
167          fi
168      done
169  
170      if [ -e "$DECRYPTED_DATA" ]; then
171          # echo "Decrypted succesfully! Decrypted data is at $DECRYPTED_DATA"
172          break
173      else
174          echo "Couldn't decrypt with that SSH key, please choose another one."
175          echo "Possible causes are:"
176          echo "You have specified the file which doesn't contain valid private key."
177          echo "Your private key doesn't match your public key in GitHub. It could happen if you've changed local ssh key recently."
178          echo "Internal error:"
179  
180          # replace report URL in $AGE_STDERR
181          STDERR_TMP="$(mktemp)"
182          cat "$AGE_STDERR" | sed -e 's#https://filippo.io/age/report#https://fluence.chat#g' > "$STDERR_TMP"
183          cat "$STDERR_TMP" > "$AGE_STDERR"
184  
185          # print Age error with replaced report URL
186          cat "$AGE_STDERR"
187      fi
188  done
189  
190  ## Prepare real ethereum address to be hashed and signed
191  ETH_ADDR_HEX_ONLY=$(echo -n "$ETHEREUM_ADDRESS" | sed -e 's/^0x//')
192  # length of ETH key is always 20 bytes
193  LENGTH="20"
194  PREFIX_HEX=$(echo -n $'\x19Ethereum Signed Message:\n'${LENGTH} | xxd -p)
195  DATA_HEX="${PREFIX_HEX}${ETH_ADDR_HEX_ONLY}"
196  
197  ## '|| true' is needed to work around this bug https://gitlab.com/kurdy/sha3sum/-/issues/2
198  HASH=$(echo -n "$DATA_HEX" | xxd -r -p | (sha3sum -a Keccak256 -t || true) | sed 's/[^[:xdigit:]].*//')
199  
200  ## Write temporary eth key to file in binary format (DER)
201  cat "$DECRYPTED_DATA" | cut -d',' -f3 | xxd -r -p -c 118 >"$ETH_KEY_DER"
202  
203  ## Convert secp256k1 key from DER (binary) to textual representation
204  
205  set +o errexit
206  openssl ec -inform der -in "$ETH_KEY_DER" 2>$OPENSSL_STDERR >"$ETH_KEY"
207  exit_code=$?
208  set -o errexit
209  
210  if [ $exit_code -ne 0 ]; then
211      echo "Failed to parse $ETH_KEY_DER with OpenSSL. Errors below may be relevant."
212      echo "==="
213      cat $OPENSSL_STDERR
214      echo "==="
215      exit 1
216  fi
217  
218  ## Sign hash of the real ethereum address with the temporary one
219  SIGNATURE_HEX=$(echo "$HASH" | xxd -r -p | openssl pkeyutl -sign -inkey "$ETH_KEY" | xxd -p -c 72)
220  
221  USER_ID=$(cat "$DECRYPTED_DATA" | cut -d',' -f1)
222  TMP_ETH_ADDR=$(cat "$DECRYPTED_DATA" | cut -d',' -f2)
223  MERKLE_PROOF=$(cat "$DECRYPTED_DATA" | cut -d',' -f4)
224  
225  echo -e "Success! Copy the line below and paste it in the browser.\n"
226  
227  # userId, tmpEthAddr, signatureHex, merkleProofHex
228  echo "${USER_ID},${TMP_ETH_ADDR},${SIGNATURE_HEX},${MERKLE_PROOF}"