Skip to content

Commit 1a35d63

Browse files
committed
Merge branch 'release/1.1.0'
2 parents 82686d3 + 015557d commit 1a35d63

7 files changed

Lines changed: 196 additions & 1 deletion

File tree

VERSIONS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# PyAuth version history:
22

3+
* 1.1.0 - Provisioning URIs
4+
- Generate provisioning URIs.
5+
- Display the QR code representation of the provisioning URI for scanning.
6+
37
* 1.0.0 - Initial release
48

59
* 0.9.10 - Implement authenticated 256-bit encryption

pyauth/AuthEntryPanel.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
## You should have received a copy of the GNU General Public License
1818
## along with this program. If not, see http://www.gnu.org/licenses/
1919

20+
import urllib
2021
import wx
2122
from AuthenticationStore import AuthenticationEntry
2223
from Logging import GetLogger
24+
from qrcode import QrCodeImage, QrCodeFrame
2325

2426
class AuthEntryPanel( wx.Panel ):
2527
"""Authentication code entry panel."""
@@ -115,6 +117,13 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.
115117
self.timer_gauge.SetMinSize( self.timer_gauge.GetSize() )
116118
sizer.Add( self.timer_gauge, 0, wx.RIGHT | wx.ALIGN_CENTER, 2 )
117119

120+
# Create our context menu
121+
self.context_menu = wx.Menu()
122+
item = self.context_menu.Append( wx.ID_ANY, "Copy provisioning URI to clipboard" )
123+
self.Bind( wx.EVT_MENU, self.OnProvisioningUri, item )
124+
item = self.context_menu.Append( wx.ID_ANY, "Display QR code image" )
125+
self.Bind( wx.EVT_MENU, self.OnQrCodeImage, item )
126+
118127
self.UpdateContents()
119128

120129
if entry != None:
@@ -125,6 +134,7 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.
125134
self.Bind( wx.EVT_TIMER, self.OnTimerTick )
126135
self.Bind( wx.EVT_ENTER_WINDOW, self.OnMouseEnter )
127136
self.Bind( wx.EVT_LEAVE_WINDOW, self.OnMouseLeave )
137+
self.Bind( wx.EVT_CONTEXT_MENU, self.OnContextMenu )
128138
self.MouseBind( wx.EVT_LEFT_DCLICK, self.OnDoubleClick )
129139
self.MouseBind( wx.EVT_LEFT_DOWN, self.OnLeftDown )
130140
self.MouseBind( wx.EVT_LEFT_UP, self.OnLeftUp )
@@ -192,6 +202,26 @@ def OnDoubleClick( self, event ):
192202
wx.Bell()
193203
event.Skip()
194204

205+
def OnContextMenu( self, event ):
206+
"""Offer choice of provisioning URL or QR code image URL from right-click menu."""
207+
pos = event.GetPosition()
208+
cl_pos = self.ScreenToClient( pos )
209+
self.PopupMenu( self.context_menu, cl_pos )
210+
211+
def OnProvisioningUri( self, event ):
212+
"""Copy the provisioning URI to the clipboard."""
213+
GetLogger().info( "%s copying provisioning URI to the clipboard.", self.GetName() )
214+
if not self.CopyProvisioningUriToClipboard():
215+
wx.Bell()
216+
event.Skip()
217+
218+
def OnQrCodeImage( self, event ):
219+
"""Display the QR code image."""
220+
GetLogger().info( "%s displaying QR code image.", self.GetName() )
221+
if not self.DisplayQrCodeImage():
222+
wx.Bell()
223+
event.Skip()
224+
195225
def OnMouseEnter( self, event ):
196226
"""Clear mouse button state when the mouse enters the panel."""
197227
self.left_down = False
@@ -366,3 +396,39 @@ def CopyCodeToClipboard( self ):
366396
GetLogger().error( "%s cannot open clipboard.", self.GetName() )
367397
sts = False
368398
return sts
399+
400+
def GetProvisioningUri( self ):
401+
return self.entry.GetKeyUri()
402+
403+
def GetQrCodeUrl( self ):
404+
qr = QrCodeImage( self.entry )
405+
return qr.GetUrl()
406+
407+
def GetQrCodeImage( self ):
408+
qr = QrCodeImage( self.entry )
409+
return qr.GetImage()
410+
411+
def CopyProvisioningUriToClipboard( self ):
412+
"""Copy the provisioning URI to the clipboard."""
413+
sts = True
414+
if wx.TheClipboard.Open():
415+
if wx.TheClipboard.SetData( wx.TextDataObject( self.GetProvisioningUri() ) ):
416+
wx.TheClipboard.Flush()
417+
else:
418+
GetLogger().error( "%s encountered an error copying the provisioning URI to the clipboard.",
419+
self.GetName() )
420+
sts = False
421+
wx.TheClipboard.Close()
422+
else:
423+
GetLogger().error( "%s cannot open clipboard.", self.GetName() )
424+
sts = False
425+
return sts
426+
427+
def DisplayQrCodeImage( self ):
428+
"""Display the QR code image."""
429+
sts = True
430+
title = self.entry.GetQualifiedAccount()
431+
image = self.GetQrCodeImage()
432+
fr = QrCodeFrame( self, wx.ID_ANY, title, image = image )
433+
fr.Show()
434+
return sts

pyauth/AuthFrame.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ def OnCreate( self, event ):
285285
self.Bind( wx.EVT_MENU, self.OnMenuNewEntry, id = wx.ID_NEW )
286286
self.Bind( wx.EVT_MENU, self.OnMenuReindex, id = self.MENU_REINDEX )
287287
self.Bind( wx.EVT_MENU, self.OnMenuRegroup, id = self.MENU_REGROUP )
288+
self.Bind( wx.EVT_MENU, self.OnMenuExportProvisioningUris, id = self.MENU_EXPORT_PROVISIONING_URIS )
288289
self.Bind( wx.EVT_MENU, self.OnMenuQuit, id = wx.ID_EXIT )
289290
self.Bind( wx.EVT_MENU, self.OnMenuCopyCode, id = self.MENU_COPY_CODE )
290291
self.Bind( wx.EVT_MENU, self.OnMenuEditEntry, id = wx.ID_EDIT )
@@ -852,6 +853,19 @@ def OnMenuRegroup( self, event ):
852853
self.populate_entries_window()
853854
self.UpdatePanelSize()
854855

856+
def OnMenuExportProvisioningUris( self, event ):
857+
"""Export provisioning URIs for all entry panels to the clipboard in one block."""
858+
uri_text = ""
859+
for panel in self.entry_panels:
860+
uri_text += panel.GetProvisioningUri() + "\n"
861+
if wx.TheClipboard.Open():
862+
if wx.TheClipboard.SetData( wx.TextDataObject( uri_text ) ):
863+
wx.TheClipboard.Flush()
864+
else:
865+
GetLogger().error( "Encountered an error exporting the provisioning URIs to the clipboard." )
866+
wx.TheClipboard.Close()
867+
else:
868+
GetLogger().error( "Cannot open clipboard." )
855869

856870
def create_menu_bar( self ):
857871
"""Create and populate the menu bar."""
@@ -867,11 +881,23 @@ def create_menu_bar( self ):
867881
self.MENU_REGROUP = mi.GetId()
868882
db_menu.AppendItem( mi )
869883

884+
# Export submenu
885+
export_menu = wx.Menu()
886+
mi = wx.MenuItem( export_menu, wx.ID_ANY, "Provisioning URIs",
887+
"Copy all provisioning URIs to the clipboard" )
888+
self.MENU_EXPORT_PROVISIONING_URIS = mi.GetId()
889+
export_menu.AppendItem( mi )
890+
mi = wx.MenuItem( export_menu, wx.ID_ANY, "QR code URLs",
891+
"Copy all QR code image URLs to the clipboard" )
892+
self.MENU_EXPORT_QRCODE_URLS = mi.GetId()
893+
export_menu.AppendItem( mi )
894+
870895
# File menu
871896
menu = wx.Menu()
872897
menu.Append( wx.ID_NEW, "&New entry\tCtrl-N", "Create a new account entry" )
873898
menu.AppendSeparator()
874899
menu.AppendSubMenu( db_menu, "DB Maintenance" )
900+
menu.AppendSubMenu( export_menu, "Export" )
875901
menu.AppendSeparator()
876902
menu.Append( wx.ID_EXIT, "E&xit\tCtrl-Q", "Exit the program" )
877903
mb.Append( menu, "&File" )

pyauth/AuthenticationStore.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import errno
2222
import string
2323
import base64
24+
import urllib
2425
import wx
2526
import pyotp
2627
from About import GetProgramName, GetVendorName
@@ -425,6 +426,15 @@ def SetAccount( self, account ):
425426
self.account = account
426427
self.modified = True
427428

429+
def GetQualifiedAccount( self ):
430+
"""Return the complete provider-qualified account identifier string."""
431+
if self.provider != '':
432+
qacct = self.provider + ':'
433+
else:
434+
qacct = ''
435+
qacct += self.account
436+
return qacct
437+
428438
def GetDigits( self ):
429439
"""Return the number of code digits."""
430440
return self.digits
@@ -473,6 +483,23 @@ def GetPeriod( self ):
473483
"""Return the time period between code changes."""
474484
return 30 # Google Authenticator uses a 30-second period
475485

486+
def GetAlgorithm( self ):
487+
"""The hashing algorithm to use."""
488+
return 'SHA1'
489+
490+
def GetKeyUri( self ):
491+
"""Get the provisioning key URI."""
492+
uri = "otpauth://totp/" + urllib.quote( self.GetQualifiedAccount() )
493+
qs_params = {}
494+
qs_params['secret'] = self.secret
495+
if self.provider != '':
496+
qs_params['issuer'] = self.provider
497+
qs_params['digits'] = self.digits
498+
qs_params['period'] = self.GetPeriod()
499+
qs_params['algorithm'] = self.GetAlgorithm()
500+
uri += '?' + urllib.urlencode( qs_params )
501+
return uri
502+
476503

477504
def GenerateNextCode( self ):
478505
"""Generate the next code in sequence from the secret."""

pyauth/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
__program_name__ = "PyAuth"
2121

2222
# Version info
23-
__version__ = '1.0.0'
23+
__version__ = '1.1.0'
2424
__version_tag__ = ''
2525
__version_status__ = ''

pyauth/qrcode.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# -*- coding: utf-8 -*-
2+
"""Generate a QR code image for an entry."""
3+
4+
## PyAuth - Google Authenticator desktop application
5+
## Copyright (C) 2016 Todd T Knarr <tknarr@silverglass.org>
6+
7+
## This program is free software: you can redistribute it and/or modify
8+
## it under the terms of the GNU General Public License as published by
9+
## the Free Software Foundation, either version 3 of the License, or
10+
## (at your option) any later version.
11+
12+
## This program is distributed in the hope that it will be useful,
13+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
## GNU General Public License for more details.
16+
17+
## You should have received a copy of the GNU General Public License
18+
## along with this program. If not, see http://www.gnu.org/licenses/
19+
20+
import wx
21+
import requests
22+
import urllib
23+
from io import BytesIO
24+
from AuthenticationStore import AuthenticationEntry
25+
from Logging import GetLogger
26+
27+
class QrCodeImage:
28+
"""Represents a QR code image."""
29+
30+
def __init__( self, entry ):
31+
self.provisioning_uri = entry.GetKeyUri()
32+
33+
def GetUrl( self ):
34+
return "https://www.google.com/chart?chs=240x240&chld=M|0&cht=qr&chl=" + urllib.quote( self.provisioning_uri )
35+
36+
def GetImage( self ):
37+
url = self.GetUrl()
38+
GetLogger().debug( "Requesting QR code image from %s", url )
39+
resp = requests.get( url )
40+
GetLogger().debug( "HTTP status: %d", resp.status_code )
41+
if resp.status_code == requests.codes.ok:
42+
input_strm = BytesIO( resp.content )
43+
image = wx.ImageFromStream( input_strm, wx.BITMAP_TYPE_PNG )
44+
else:
45+
GetLogger().error( "HTTP error %d", resp.status_code )
46+
GetLogger().error( "Error response body:\n%s", resp.text )
47+
image = None
48+
return image
49+
50+
51+
class QrCodeFrame( wx.Frame ):
52+
53+
def __init__( self, parent, id, title, pos = wx.DefaultPosition, size = wx.DefaultSize,
54+
style = wx.DEFAULT_FRAME_STYLE, name = wx.FrameNameStr, image = None, border = 0 ):
55+
56+
self.border = border
57+
self.x_loc = border
58+
self.y_loc = border
59+
self.bitmap = image.ConvertToBitmap()
60+
61+
wx.Frame.__init__( self, parent, id, title, pos, size, style, name )
62+
63+
client_size = self.bitmap.GetSize()
64+
client_size.IncBy( self.border * 2, self.border * 2 )
65+
self.SetClientSize( client_size )
66+
67+
self.Bind( wx.EVT_PAINT, self.OnPaint )
68+
69+
def OnPaint( self, event ):
70+
dc = wx.PaintDC( self )
71+
dc.DrawBitmap( self.bitmap, self.x_loc, self.y_loc )

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282

8383
install_requires = [
8484
#'wxPython>=3.0',
85+
'requests>=2.10',
8586
'pyotp>=2.0.1',
8687
'pycrypto>=2.6.1',
8788
'cryptography>=1.3'

0 commit comments

Comments
 (0)