Skip to content

Commit

Permalink
feat: add instagram data source (#118)
Browse files Browse the repository at this point in the history
## Description


This PR adds Instagram data source for themis.

## Checklist
- [x] Targeted PR against correct branch.
- [ ] Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
- [ ] Wrote unit tests.
- [x] Updated the documentation. 
- [x] Re-reviewed `Files changed` in the Github PR explorer.
  • Loading branch information
dadamu authored Aug 6, 2023
1 parent 616beab commit a2c780e
Show file tree
Hide file tree
Showing 3 changed files with 424 additions and 0 deletions.
53 changes: 53 additions & 0 deletions data-sources/docs/instagram.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Instagram

Users can connect their Instagram account by posting a link to the verification data inside the biography.
The verification data must be a JSON object formed as described inside ["Verification data"](../../README.md#verification-data).

## Example biography

An example biography can be found [here](https://www.instagram.com/test_desmos_user).

## Verification process

The verification process on Instagram is made of the following steps:

1. The user uploads their verification data to an online storage (eg. PasteBin).
2. The user links the online storage URL to their account by putting it inside their account biography.
3. The user requests Themis to cache their user profile data by an access token with the permission requesting for their profile.
4. The user performs a Desmos transaction telling that they want to link the Instagram account to the Desmos one, and provides the proper call data.

Once that's done, what will happen is the following:

1. Desmos will send the call data to Band Protocol, asking to get the Instagram user username and the signature provided by the user.
2. Band Protocol will call the appropriate data source that will use our APIs to get the data from the account biography.
3. Once downloaded, the data source will check the validity of the data and return to Desmos the user username and signature.
4. If the Instagram user username matches the one provided by the user, and the signature is valid against the user public key, the Desmos and Instagram account will be linked together.

## Data source call data

When asking to verify the ownership of a Instagram account, the data source call data must be a JSON object formed as follows:

```json
{
"username": "<Instagram user username to be verified>"
}
```

Example:

```json
{
"username":"test_desmos_user"
}
```

Hex encoded:
```
7b22757365726e616d65223a22746573745f6465736d6f735f75736572227d
```

Example execution:

```shell
python Instagram.py 7b22757365726e616d65223a22746573745f6465736d6f735f75736572227d
```
190 changes: 190 additions & 0 deletions data-sources/instagram.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#!/usr/bin/env python3
import json
import sys
import requests
import re
from typing import Optional
import cryptography.hazmat.primitives.asymmetric.utils as crypto
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
import hashlib

ENDPOINT = "https://themis.mainnet.desmos.network/instagram"
HEADERS = {"Content-Type": "application/json"}


class CallData:
"""
Contains the data that has been used to call the script
"""

def __init__(self, username: str):
self.username = username


class VerificationData:
"""
Contains the data needed to verify the proof submitted by the user.
"""

def __init__(self, address: str, pub_key: str, value: str, signature: str):
self.address = address
self.pub_key = pub_key
self.signature = signature
self.value = value


def get_urls_from_biography(user: str) -> [str]:
"""
Returns all the URLs that are found inside the biography of the user having the given user username.
:param user: Username of the Instagram user.
:return: List of URLs that are found inside the biography
"""
url = f"{ENDPOINT}/users/{user}"
result = requests.request("GET", url, headers=HEADERS).json()
return re.findall(r'(https?://[^\s]+)', result['biography'])


def get_signature_from_url(url: str) -> Optional[VerificationData]:
"""
Tries getting the signature object linked to the given URL.
:param url: URL that should contain the signature object.
:return: A dictionary containing 'valid' to tell whether the search was valid, and an optional 'data' containing
the signature object.
"""
try:
result = requests.request("GET", url, headers=HEADERS).json()
if validate_json(result):
return VerificationData(
result['address'],
result['pub_key'],
result['value'],
result['signature'],
)
else:
return None
except ValueError:
return None


def validate_json(json: dict) -> bool:
"""
Tells whether or not the given JSON is a valid signature JSON object.
:param json: JSON object to be checked.
:return: True if the provided JSON has a valid signature schema, or False otherwise.
"""
return all(key in json for key in ['value', 'pub_key', 'signature', 'address'])


def verify_signature(data: VerificationData) -> bool:
"""
Verifies the signature using the given pubkey and value.
:param data: Data used to verify the signature.
:return True if the signature is valid, False otherwise
"""
if len(data.signature) != 128:
return False

try:
# Create signature for dss signature
(r, s) = int(data.signature[:64], 16), int(data.signature[64:], 16)
sig = crypto.encode_dss_signature(r, s)

# Create public key instance
public_key = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), bytes.fromhex(data.pub_key))

# Verify the signature
public_key.verify(sig, bytes.fromhex(data.value), ec.ECDSA(hashes.SHA256()))
return True
except Exception:
return False


def verify_address(data: VerificationData) -> bool:
"""
Verifies that the given address is the one associated with the provided HEX encoded compact public key.
:param data: Data used to verify the address
"""
s = hashlib.new("sha256", bytes.fromhex(data.pub_key)).digest()
r = hashlib.new("ripemd160", s).digest()
return data.address.upper() == r.hex().upper()


def check_values(values: dict) -> CallData:
"""
Checks the validity of the given dictionary making sure it contains the proper data.
:param values: Dictionary that should be checked.
:return: A CallData instance.
"""
if "username" not in values:
raise Exception("Missing 'username' value")

return CallData(values["username"])


def main(args: str):
"""
Gets the signature data from Instagram, after the user has provided it through the Hephaestus bot.
:param args Hex encoded JSON object containing the arguments to be used during the execution.
In order to be valid, the encoded JSON object must contain one field named "username" that represents the Instagram
username of the account to be connected.
Example argument value:
7B22757365726E616D65223A22526963636172646F204D6F6E7461676E696E2335343134227D
This is the hex encoded representation of the following JSON object:
```json
{
"username":"test_user"
}
```
:param args: JSON encoded parameters used during the execution.
:return The signed value and the signature as a single comma separated string.
:raise Exception if anything is wrong during the process. This can happen if:
1. The Instagram user has not started the connection using the Hephaestus bot
2. The provided signature is not valid
3. The provided address is not linked to the provided public key
"""

decoded = bytes.fromhex(args)
json_obj = json.loads(decoded)
call_data = check_values(json_obj)

# Get the URLs to check from the user biography
urls = get_urls_from_biography(call_data.username)
if len(urls) == 0:
raise Exception(f"No URL found inside {call_data.username} biography")

# Find the signature following the URLs
data = None
for url in urls:
result = get_signature_from_url(url)
if result is not None:
data = result
break

if data is None:
raise Exception(f"No valid signature data found inside {call_data.username} biography")

# Verify the signature
signature_valid = verify_signature(data)
if not signature_valid:
raise Exception("Invalid signature")

# Verify the address
address_valid = verify_address(result)
if not address_valid:
raise Exception("Invalid address")

return f"{result.value},{result.signature},{call_data.username}"


if __name__ == "__main__":
try:
print(main(*sys.argv[1:]))
except Exception as e:
print(str(e), file=sys.stderr)
sys.exit(1)
Loading

0 comments on commit a2c780e

Please sign in to comment.