dlenski / python-vipaccess

A free software implementation of Symantec's VIP Access application and protocol
Apache License 2.0
831 stars 84 forks source link

Google Authenticator generates different TOTP codes than vipaccess show/oathtool? #56

Closed evadeflow closed 3 years ago

evadeflow commented 3 years ago

I just added 2FA with one of my providers using python-vipaccess instead of the Symantec app. While on the phone with their support rep, he asked me to confirm that I could log out and then log back in again before ending the call. I did so, and when prompted for the TOTP code, I used the output from vipaccess show. And eureka—it worked!

Afterwards, I used this command to generate a QR code for Google Authenticator:

$ qrencode -t utf8 $(vipaccess uri | grep otpauth)

I was able to import it successfully using the QR code, but I was disappointed to discover that it generates a different TOTP code than vipaccess show. Thinking perhaps it was a problem with the QR-code generation step, I tried manually entering the 32-character 'secret' portion from the output of vipaccess uri into Google Authenticator, and discovered that it generates the exact same (incorrect) TOTP codes as the QR code did.

As a further test, I entered the same secret into the 1Password app (which was my end goal all along) and saw that it generates the same (wrong) codes as Google Authenticator. Interesting! Initially, I thought this might have something to do with issue #39. Then I discovered that this command generates correct TOTP codes:

$ oathtool -b --totp YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY

So... Google Authenticator and 1Password show the 'wrong' codes for the same secret compared to oathtool and vipaccess show. That feels... like user error on my part(?) I suspect I'm about to learn something new/important about TOTPs, but I've no idea what that is yet. Can anybody shed light on this for me? 😕

dlenski commented 3 years ago

Google Authenticator, 1Password import via URI, import via secret alone, … I suspect I'm about to learn something new/important about TOTPs, but I've no idea what that is yet. Can anybody shed light on this for me? :confused:

Me too! It seems like you've done your homework and tried different possibilities.

Any chance that your mobile device's clock is way off?

Can you verify that the token you generated matches this format: 6-digit, SHA1 algorithm, 30 second period?

otpauth://totp/VIP%20Access:$TOKEN_ID?secret=$BASE32_SECRET&digits=6&algorithm=SHA1&image=https%3A%2F%2Fraw.githubusercontent.com%2Fdlenski%2Fpython-vipaccess%2Fmaster%2Fvipaccess.png&period=30

I use FreeOTP as a mobile authenticator app, and cannot reproduce this issue. I generated a new one with vipaccess provision:

$ vipaccess provision -p
Generating request...
Fetching provisioning response from Symantec server...
Getting token from response...
Decrypting token...
Checking token against Symantec server...
Credential created successfully:
    otpauth://totp/VIP%20Access:SYMC47799324?secret=SM7AGDVSSX3OXTZO4KIVXOPU4DVMZBJ2&digits=6&algorithm=SHA1&image=https%3A%2F%2Fraw.githubusercontent.com%2Fdlenski%2Fpython-vipaccess%2Fmaster%2Fvipaccess.png&period=30
This credential expires on this date: 2024-04-15T16:07:18.591Z

You will need the ID to register this credential: SYMC47799324

You can use oathtool to generate the same OTP codes
as would be produced by the official VIP Access apps:

    oathtool    -b --totp SM7AGDVSSX3OXTZO4KIVXOPU4DVMZBJ2  # output one code
    oathtool -v -b --totp SM7AGDVSSX3OXTZO4KIVXOPU4DVMZBJ2  # ... with extra information

I imported it into FreeOTP via QR code, and verified that the codes from the mobile app match the oathtool output and the vipaccess show -s SM7AGDVSSX3OXTZO4KIVXOPU4DVMZBJ2 output.

evadeflow commented 3 years ago

Thanks for responding! My token seems to have the correct format:

$ vipaccess uri
Token URI: 

    otpauth://totp/Symantec:SYMCXXXXXXXX?secret=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY&digits=6&algorithm=SHA1&image=https%3A%2F%2Fraw.githubusercontent.com%2Fdlenski%2Fpython-vipaccess%2Fmaster%2Fvipaccess.png&period=30

(For clarity: yes, I obfuscated the ID and secret in case I wind up continuing to use this credential.)

It's really odd, but both oathtool and vipaccess show continue to give the same result with whatever secret key I feed them:

image

And Google Authenticator and the 1Password plugin for Firefox also agree with each other when fed the same key:

Google Authenticator 1Password Plugin
image image

But as you can see, they generate a different TOTP than oathtool and vipaccess show. For the record, I waited until the beginning of a new 30-second 'window' before running the commands in the first screenshot, and I made the second two screenshots within the next 15 seconds, so they should agree. (Well... if I were King, and not confused about how TOTP actually works under the covers, they'd agree. But clearly, that's not the world we're living in. 👑)

Now for the really weird thing: I downloaded the FreeOTP app you mentioned and scanned the QR code for my credential into it. And it's generating codes that match Google Authenticator and 1Password! 🤦 A difference in the system clocks between my PC and mobile phone would be a nice, tidy explanation for this, but... they both seem to be correct. (I have to sign in to a telecon, but I'll try to add another comment later with some more details. And I'll confirm that the clocks aren't [somehow] simply off by, say, 90 seconds or something like that—I don't think that's the case, but... it's too neat of an explanation not to do an exhaustive check...)

dlenski commented 3 years ago

A difference in the system clocks between my PC and mobile phone would be a nice, tidy explanation for this

Everything running on your phone (Google Auth, 1Password, FreeOTP) gives one result.

Everything running on your computer (vipaccess, oathtool) gives a different result.

What else could it possibly be⁉️

Run vipaccess provision -p to try to generate a new throwaway token.

evadeflow commented 3 years ago

Wow, okay, this is pretty interesting. I just did a careful time check of all the system clocks involved in this issue, which include the clocks for:

The first two were in agreement to within a fraction of a second, most likely because they both sync to an external NTP server. The last one (i.e., the VM) used to sync to the same NTP server as its host PC; however, I discovered that—since I started working exclusively from home over a VPN connection—this is no longer the case. (For whatever reason, the NTP server hostname doesn't resolve correctly within the VM when I'm connected via the VPN.) Anyway, I discovered that the VM's clock lagged the other systems by around 88 seconds. And by watching the output from Google Authenticator for several minutes, while also running the following command in my VM:

$ while true; do
  vipaccess show  
  sleep 30
done

I discovered that the stream of codes coming from the VM was the same as the stream from Google Authenticator (and 1Password), but offset by two 'cycles'. In other words, each code shown in Google Authenticator or 1Password was also displayed in my VM, only they didn't show up there until 60+ seconds or so later.

It's tempting to say, "Oh, okay, garden variety clock-sync error, nothing to see here, folks!" But... I'm running 1Password on my host system, and I did verify that the system clock was accurate to within one second before creating this issue, so... I just couldn't understand why those codes were being rejected. And: when I discovered the time mismatch on my VM and updated the system time to match the other system clocks, the external server began rejecting the TOTP codes emitted by vipaccess show—codes that it had previously accepted! I thought: "Great. Now I'm locked out of my account." 😑 But after rejecting two consecutive codes, the server then displayed this dialog:

image

After entering two consecutive codes, everything is working fine now. I can log in on the first try, every time, using the TOTP codes from 1Password. This leads me to the following wild speculation about what may have happened:

As I said, this is sheer speculation, and I'm sure some of the details are incorrect, but... something along these lines definitely happened. I would love to know more of the nitty-gritty details of this, and why I've never seen a dialog like the one above from any of the other 2FA-enabled services I use. I always just kind of assumed that Google, Github and other providers used some kind of 'grace window' around the current time, and would honor a provided token if it was one or two 'slots' ahead or behind the current time, i.e., "Well, this token isn't currently valid, but it [would have been | will be ] 90 seconds [ago | from now], so we'll just wave it through." Symantec seems to handle this a little bit differently(??)

Anyway, thanks for listening to the ravings of a madman while I figured this all out. I'm really glad to now be independent of that silly Symantec app—thanks so much to you and @cyrozap for making it possible! I think it's okay to close this ticket as a mere curiosity at this point. The only thing you might want to consider is adding an admonition to the README stating something like:

WARNING: Ensure system clock is accurate!

When generating credentials using vipaccess provision, it's critical that the system time of the machine you're using is accurate, as subtle problems may occur otherwise.

(I'd love to be more specific than that, but... I'm not really sure exactly what went wrong...)

dlenski commented 3 years ago

Anyway, I discovered that the VM's clock lagged the other systems by around 88 seconds.

Ding ding ding :wink:

Note that there's a vipaccess check, which will check whether the token is in-sync with Symantec's servers. (Which may not have helped in this case, because it seems you managed to register your token with an off-by-88-seconds clock, thus likely baking in the off-by-88-seconds interval.)

I always just kind of assumed that Google, Github and other providers used some kind of 'grace window' around the current time, and would honor a provided token if it was one or two 'slots' ahead or behind the current time, i.e., "Well, this token isn't currently valid, but it [would have been | will be ] 90 seconds [ago | from now], so we'll just wave it through." Symantec seems to handle this a little bit differently(??)

Yeah, there may be. If you (consistently?) give them codes that are off by ±1 period, they may decide that your system clock is shifted, and start expecting you to give them codes that are off by that amount.

Servers that use hardware tokens definitely have to do this, because a hardware token may drift by a substantial fraction of the period during its battery lifetime of 5-10 years.

Your "wild speculation" seems about right.

dlenski commented 3 years ago

I created my credential using vipaccess provision on a VM whose clock was 88 seconds behind

As of d4c618c4a8f000dc3f1e6d57fa764bb96abe455e, vipaccess provision is supposed to warn you if the clock is skewed. It should not succeed if the clock is >30 seconds off.

So how could it have succeeded when you ran it:interrobang:

Well, I tried to figure this out, by kludging in various time offsets when validating the newly created token, which should cause the validation to fail…

diff --git a/vipaccess/__main__.py b/vipaccess/__main__.py
index 33826d1..03d2bee 100644
--- a/vipaccess/__main__.py
+++ b/vipaccess/__main__.py
@@ -55,10 +55,12 @@ def provision(p, args):
     otp_secret = vp.decrypt_key(otp_token['iv'], otp_token['cipher'])
     otp_secret_b32 = base64.b32encode(otp_secret).upper().decode('ascii')
     print("Checking token against Symantec server...")
-    if not vp.check_token(otp_token, otp_secret, session):
+    if not vp.check_token(otp_token, otp_secret, session, timestamp=time.time() + OFFSET_GOES_HERE):
         print("WARNING: Something went wrong--the token could not be validated.\n",
               "    (check your system time; it differs from the server's by %d seconds)\n" % otp_token['timeskew'],
               file=sys.stderr)

And what I find is… you have to put in an offset of about ±3000 seconds (50 minutes… :scream: :exclamation:) before the server won't just “accept” your time skew, and “bake it in” to the newly-created token. Woof. :-1: :-1:

So that explains how/why your 88-second skew accepted initially. For everyone's sanity, let's not be as lax as the Symantec server is: 7a979c42bc6845cca68db9b4d0c58fae13b9f7b9