Skip to content

Commit

Permalink
Unable to login using 2FA [#276] Added TOTP, TOTPMixin and TOTPTestCase
Browse files Browse the repository at this point in the history
  • Loading branch information
adw0rd committed Aug 21, 2021
1 parent e40d33a commit f22cee7
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 4 deletions.
2 changes: 2 additions & 0 deletions instagrapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
)
from instagrapi.mixins.story import StoryMixin
from instagrapi.mixins.timeline import ReelsMixin
from instagrapi.mixins.totp import TOTPMixin
from instagrapi.mixins.user import UserMixin
from instagrapi.mixins.video import DownloadVideoMixin, UploadVideoMixin

Expand Down Expand Up @@ -59,6 +60,7 @@ class Client(
UploadClipMixin,
ReelsMixin,
BloksMixin,
TOTPMixin,
):
proxy = None
logger = logging.getLogger("instagrapi")
Expand Down
3 changes: 2 additions & 1 deletion instagrapi/mixins/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ def account_change_picture(self, path: Path) -> UserShort:
return extract_user_short(result["user"])

def news_inbox_v1(self, mark_as_seen: bool = False) -> dict:
"""Get old and new stories as is
"""
Get old and new stories as is
Parameters
----------
Expand Down
4 changes: 2 additions & 2 deletions instagrapi/mixins/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,12 +334,12 @@ def login(self, username: str, password: str, relogin: bool = False, verificatio
----------
username: str
Instagram Username
password: str
Instagram Password
relogin: bool
Whether or not to re login, default False
verification_code: str
2FA verification code
Returns
-------
Expand Down
138 changes: 138 additions & 0 deletions instagrapi/mixins/totp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import base64
import datetime
import hashlib
import hmac
import time
from typing import Any, List, Optional


class TOTP:
"""
Base class for OTP handlers.
"""
def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None,
issuer: Optional[str] = None) -> None:
self.digits = digits
self.digest = digest
self.secret = s
self.name = name or 'Secret'
self.issuer = issuer
self.interval = 30

def generate_otp(self, input: int) -> str:
"""
:param input: the HMAC counter value to use as the OTP input.
Usually either the counter, or the computed integer based on the Unix timestamp
"""
if input < 0:
raise ValueError('input must be positive integer')
hasher = hmac.new(self.byte_secret(), self.int_to_bytestring(input), self.digest)
hmac_hash = bytearray(hasher.digest())
offset = hmac_hash[-1] & 0xf
code = ((hmac_hash[offset] & 0x7f) << 24 |
(hmac_hash[offset + 1] & 0xff) << 16 |
(hmac_hash[offset + 2] & 0xff) << 8 |
(hmac_hash[offset + 3] & 0xff))
str_code = str(code % 10 ** self.digits)
while len(str_code) < self.digits:
str_code = '0' + str_code
return str_code

def byte_secret(self) -> bytes:
secret = self.secret
missing_padding = len(secret) % 8
if missing_padding != 0:
secret += '=' * (8 - missing_padding)
return base64.b32decode(secret, casefold=True)

@staticmethod
def int_to_bytestring(i: int, padding: int = 8) -> bytes:
"""
Turns an integer to the OATH specified
bytestring, which is fed to the HMAC
along with the secret
"""
result = bytearray()
while i != 0:
result.append(i & 0xFF)
i >>= 8
# It's necessary to convert the final result from bytearray to bytes
# because the hmac functions in python 2.6 and 3.3 don't work with
# bytearray
return bytes(bytearray(reversed(result)).rjust(padding, b'\0'))

def code(self):
"""
Generate TOTP code
"""
now = datetime.datetime.now()
timecode = int(time.mktime(now.timetuple()) / self.interval)
return self.generate_otp(timecode)


class TOTPMixin:

def totp_generate_seed(self) -> str:
"""
Generate 2FA TOTP seed
Returns
-------
str
TOTP seed (token, secret key)
"""
result = self.private_request(
"accounts/generate_two_factor_totp_key/",
data=self.with_default_data({})
)
return result["totp_seed"]

def totp_enable(self, verification_code: str) -> List[str]:
"""
Enable TOTP 2FA
Parameters
----------
verification_code: str
2FA verification code
Returns
-------
List[str]
Backup codes
"""
result = self.private_request(
"accounts/enable_totp_two_factor",
data=self.with_default_data({'verification_code': verification_code})
)
return result["backup_codes"]

def totp_disable(self) -> bool:
"""
Disable TOTP 2FA
Returns
-------
bool
"""
result = self.private_request(
"accounts/disable_totp_two_factor",
data=self.with_default_data({})
)
return result["status"] == "ok"

def totp_generate_code(self, seed: str) -> str:
"""
Disable TOTP 2FA
Parameters
----------
seed: str
TOTP seed (token, secret key)
Returns
-------
str
TOTP code
"""
return TOTP(seed).code()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

setup(
name='instagrapi',
version='1.11.1',
version='1.12.0',
author='Mikhail Andreev',
author_email='[email protected]',
license='MIT',
Expand Down
9 changes: 9 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1447,6 +1447,15 @@ def test_story_info(self):
# }
# self.assertTrue(self.api.bloks_change_password("2r9j20r9j4230t8hj39tHW4"))

class TOTPTestCase(ClientPrivateTestCase):

def test_totp_code(self):
seed = self.api.totp_generate_seed()
code = self.api.totp_generate_code(seed)
self.assertIsInstance(code, str)
self.assertTrue(code.isdigit())
self.assertEqual(len(code), 6)


if __name__ == '__main__':
unittest.main()

0 comments on commit f22cee7

Please sign in to comment.