Initial working version
This commit is contained in:
		| @@ -2,5 +2,6 @@ | ||||
| <gresources> | ||||
| 	<gresource prefix="/eu/polonkai/gergely/gauthenticator"> | ||||
| 		<file preprocess="xml-stripblanks">gauth-window.ui</file> | ||||
| 		<file preprocess="xml-stripblanks">otp-row.ui</file> | ||||
| 	</gresource> | ||||
| </gresources> | ||||
|   | ||||
							
								
								
									
										66
									
								
								data/otp-row.ui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								data/otp-row.ui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!-- Generated with glade 3.20.0 --> | ||||
| <interface> | ||||
|   <requires lib="gtk+" version="3.12"/> | ||||
|   <object class="GtkImage" id="copy_image"> | ||||
|     <property name="visible">True</property> | ||||
|     <property name="can_focus">False</property> | ||||
|     <property name="icon_name">edit-copy</property> | ||||
|   </object> | ||||
|   <template class="GAuthenticatorOTPRow" parent="GtkGrid"> | ||||
|     <property name="visible">True</property> | ||||
|     <property name="can_focus">False</property> | ||||
|     <child> | ||||
|       <object class="GtkLabel" id="code"> | ||||
|         <property name="visible">True</property> | ||||
|         <property name="can_focus">False</property> | ||||
|         <property name="hexpand">True</property> | ||||
|         <property name="xpad">5</property> | ||||
|         <property name="label">000000</property> | ||||
|         <property name="xalign">0</property> | ||||
|         <attributes> | ||||
|           <attribute name="weight" value="bold"/> | ||||
|         </attributes> | ||||
|       </object> | ||||
|       <packing> | ||||
|         <property name="left_attach">0</property> | ||||
|         <property name="top_attach">1</property> | ||||
|         <property name="width">2</property> | ||||
|       </packing> | ||||
|     </child> | ||||
|     <child> | ||||
|       <object class="GtkButton"> | ||||
|         <property name="visible">True</property> | ||||
|         <property name="can_focus">True</property> | ||||
|         <property name="receives_default">True</property> | ||||
|         <property name="action_name">copy-code</property> | ||||
|         <property name="image">copy_image</property> | ||||
|       </object> | ||||
|       <packing> | ||||
|         <property name="left_attach">2</property> | ||||
|         <property name="top_attach">0</property> | ||||
|         <property name="height">2</property> | ||||
|       </packing> | ||||
|     </child> | ||||
|     <child> | ||||
|       <object class="GtkLabel" id="provider_name"> | ||||
|         <property name="visible">True</property> | ||||
|         <property name="can_focus">False</property> | ||||
|       </object> | ||||
|       <packing> | ||||
|         <property name="left_attach">0</property> | ||||
|         <property name="top_attach">0</property> | ||||
|       </packing> | ||||
|     </child> | ||||
|     <child> | ||||
|       <object class="GtkLabel" id="account_name"> | ||||
|         <property name="visible">True</property> | ||||
|         <property name="can_focus">False</property> | ||||
|       </object> | ||||
|       <packing> | ||||
|         <property name="left_attach">1</property> | ||||
|         <property name="top_attach">0</property> | ||||
|       </packing> | ||||
|     </child> | ||||
|   </template> | ||||
| </interface> | ||||
| @@ -2,10 +2,16 @@ bin_PROGRAMS = gauthenticator | ||||
|  | ||||
| gresource_file = $(top_srcdir)/data/gauthenticator.gresource.xml | ||||
|  | ||||
| gauthenticator_SOURCES = main.vala gauth-app.vala gauth-window.vala | ||||
| BUILT_SOURCES = resources.c | ||||
| gauthenticator_SOURCES = $(BUILT_SOURCES) base32.vala otp.vala otp-row.vala main.vala gauth-app.vala gauth-window.vala | ||||
| gauthenticator_CPPFLAGS = $(GAUTHENTICATOR_CFLAGS) | ||||
| gauthenticator_VALAFLAGS = --pkg gtk+-3.0 --gresources $(gresource_file) --target-glib=2.38 | ||||
|  | ||||
| gauthenticator_LDADD = $(GAUTHENTICATOR_LIBS) | ||||
| gauthenticator_LDADD = $(GAUTHENTICATOR_LIBS) -lm | ||||
|  | ||||
| -include $(top_srcdir)/git.mk | ||||
|  | ||||
| resource_files = $(shell $(GLIB_COMPILE_RESOURCES) --generate-dependencies --sourcedir=$(top_srcdir)/data $(gresource_file)) | ||||
| resources.c: $(gresource_file) $(resource_files) | ||||
| 	$(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) --target=$@ --sourcedir=$(top_srcdir)/data --generate-source $< | ||||
|  | ||||
|   | ||||
							
								
								
									
										175
									
								
								src/base32.vala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								src/base32.vala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | ||||
| namespace Base32 { | ||||
|     public errordomain Base32Error { | ||||
|         INCORRECT_PADDING, | ||||
|         INCORRECT_MAP01; | ||||
|     } | ||||
|  | ||||
|     private const string base32_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; | ||||
|     private Array<string> _base32_tab = null; | ||||
|     private GLib.HashTable<uint8, uint8> _b32rev = null; | ||||
|  | ||||
|     public GLib.Bytes b32encode(GLib.Bytes input) { | ||||
|         if (_base32_tab == null) { | ||||
|             _base32_tab = new Array<string>(); | ||||
|             var b32tab = new GLib.Bytes(base32_alphabet.data); | ||||
|  | ||||
|  | ||||
|             foreach (var a in b32tab.get_data()) { | ||||
|                 foreach (var b in b32tab.get_data()) { | ||||
|                     _base32_tab.append_val("%c%c".printf(a, b)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         var leftover = input.length % 5; | ||||
|         var s = new GLib.ByteArray(); | ||||
|         s.append(input.get_data()); | ||||
|  | ||||
|         if (leftover > 0) { | ||||
|             var padding = new uint8[leftover + 1]; | ||||
|  | ||||
|             for (var i = 0; i <= leftover; i++) { | ||||
|                 padding[i] = 0; | ||||
|             } | ||||
|  | ||||
|             s.append(padding); | ||||
|         } | ||||
|  | ||||
|         string encoded = ""; | ||||
|         var length = 0; | ||||
|  | ||||
|         for (var i = 0; i < s.len; i += 5) { | ||||
|             var next5 = s.data[i:i + 5]; | ||||
|             uint64 c = ((uint64)next5[0] << 32) + | ||||
|                        ((uint64)next5[1] << 24) + | ||||
|                        ((uint64)next5[2] << 16) + | ||||
|                        ((uint64)next5[3] << 8) + | ||||
|                        (uint64)next5[4]; | ||||
|  | ||||
|             encoded += _base32_tab.index((uint)(c >> 30)); | ||||
|             encoded += _base32_tab.index((uint)((c >> 20) & 0x3ff)); | ||||
|             encoded += _base32_tab.index((uint)((c >> 10) & 0x3ff)); | ||||
|             encoded += _base32_tab.index((uint)(c & 0x3ff)); | ||||
|             length += 8; | ||||
|         } | ||||
|  | ||||
|         int padder = 0; | ||||
|         if (leftover == 1) { | ||||
|             padder = -6; | ||||
|         } else if (leftover == 2) { | ||||
|             padder = -4; | ||||
|         } else if (leftover == 3) { | ||||
|             padder = -3; | ||||
|         } else if (leftover == 4) { | ||||
|             padder = -1; | ||||
|         } | ||||
|         for (var i = padder; i < 0; i++) { | ||||
|             encoded.data[encoded.length + i] = '='; | ||||
|         } | ||||
|  | ||||
|         return new GLib.Bytes(encoded.data); | ||||
|     } | ||||
|  | ||||
|     public GLib.Bytes b32decode(GLib.Bytes input, bool casefold, char map01) throws Base32Error { | ||||
|         if (_b32rev == null) { | ||||
|             _b32rev = new GLib.HashTable<uint8, uint8>(null, null); | ||||
|  | ||||
|             for (var i = 0; i < base32_alphabet.length; i++) { | ||||
|                 _b32rev[base32_alphabet[i]] = i; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (input.length % 8 != 0) { | ||||
|             throw new Base32Error.INCORRECT_PADDING("Incorrect padding"); | ||||
|         } | ||||
|  | ||||
|         var s = new GLib.ByteArray(); | ||||
|  | ||||
|         if (map01 != 0) { | ||||
|             if ((map01 != 'L') && (map01 != 'I')) { | ||||
|                 throw new Base32Error.INCORRECT_MAP01("Incorrect map01 value"); | ||||
|             } | ||||
|  | ||||
|             var translated = new uint8[input.length]; | ||||
|  | ||||
|             var idata = input.get_data(); | ||||
|  | ||||
|             for (var i = 0; i < input.length; i++) { | ||||
|                 if (idata[i] == 'O') { | ||||
|                     translated[i] = '0'; | ||||
|                 } else if (idata[i] == map01) { | ||||
|                     translated[i] = '1'; | ||||
|                 } else { | ||||
|                     translated[i] = idata[i]; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             s.append(translated); | ||||
|         } else { | ||||
|             s.append(input.get_data()); | ||||
|         } | ||||
|  | ||||
|         if (casefold) { | ||||
|             for (var i = 0; i < s.len; i++) { | ||||
|                 s.data[i] = ((char)s.data[i]).toupper(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         var padchars = 0; | ||||
|         while (s.data[s.len - padchars - 1] == '=') { | ||||
|             padchars++; | ||||
|         } | ||||
|  | ||||
|         s.remove_range(s.len - padchars, padchars); | ||||
|  | ||||
|  | ||||
|         var decoded = new GLib.ByteArray(); | ||||
|  | ||||
|         uint64 acc = 0; | ||||
|  | ||||
|         for (var i = 0; i < input.length; i += 8) { | ||||
|             var quanta = s.data[i: i + 8]; | ||||
|             acc = 0; | ||||
|  | ||||
|             for (var j = 0; j < 8; j++) { | ||||
|                 var c = quanta[j]; | ||||
|                 acc = (acc << 5) + _b32rev[c]; | ||||
|             } | ||||
|  | ||||
|             uint8 res[5]; | ||||
|  | ||||
|             for (var j = 0; j < 5; j++) { | ||||
|                 res[j] = (uint8)((acc >> (32 - j * 8)) & 0xff); | ||||
|             } | ||||
|  | ||||
|             decoded.append(res); | ||||
|         } | ||||
|  | ||||
|         if (padchars > 0) { | ||||
|             uint8 last[5]; | ||||
|  | ||||
|             for (var j = 0; j < 5; j++) { | ||||
|                 last[j] = (uint8)((acc >> (32 - j * 8)) & 0xff); | ||||
|             } | ||||
|  | ||||
|             var pad_offset = 0; | ||||
|  | ||||
|             if (padchars == 1) { | ||||
|                 pad_offset = -1; | ||||
|             } else if (padchars == 3) { | ||||
|                 pad_offset = -2; | ||||
|             } else if (padchars == 4) { | ||||
|                 pad_offset = -3; | ||||
|             } else if (padchars == 6) { | ||||
|                 pad_offset = -4; | ||||
|             } else { | ||||
|                 throw new Base32Error.INCORRECT_PADDING("Incorrect padding"); | ||||
|             } | ||||
|  | ||||
|             decoded.remove_range(decoded.len - 5, 5); | ||||
|             decoded.append(last[0:5 + pad_offset]); | ||||
|         } | ||||
|  | ||||
|         return new GLib.Bytes(decoded.data); | ||||
|     } | ||||
| } | ||||
| @@ -1,11 +1,48 @@ | ||||
| namespace GAuthenticator { | ||||
| 	[GtkTemplate (ui = "/eu/polonkai/gergely/gauthenticator/gauth-window.ui")] | ||||
| 	class Window : Gtk.ApplicationWindow { | ||||
| 		[GtkChild] | ||||
| 		private Gtk.ProgressBar countdown; | ||||
|     [GtkTemplate (ui = "/eu/polonkai/gergely/gauthenticator/gauth-window.ui")] | ||||
|     class Window : Gtk.ApplicationWindow { | ||||
|         [GtkChild] | ||||
|         private Gtk.ProgressBar countdown; | ||||
|  | ||||
| 		public Window(GAuthenticator.App app) { | ||||
| 			Object(application: app); | ||||
| 		} | ||||
| 	} | ||||
|         [GtkChild] | ||||
|         private Gtk.ListBox auth_list; | ||||
|  | ||||
|         private List<OTPRow> rows = null; | ||||
|  | ||||
|         private void update_totps(bool first = false) { | ||||
|             foreach (var row in rows) { | ||||
|                 row.update(first); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private void check_update_totps() { | ||||
|             var now = new GLib.DateTime.now_utc(); | ||||
|             var remaining = 30.0 - (now.get_seconds() % 30.0); | ||||
|  | ||||
|             if (remaining <= 1.0 / 24.0) { | ||||
|                 update_totps(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private bool update_countdown() { | ||||
|             var timestamp = new GLib.DateTime.now_local(); | ||||
|             var current_seconds = timestamp.get_seconds(); | ||||
|             var start_seconds = (current_seconds > 30.0) ? 30.0 : 0.0; | ||||
|  | ||||
|             countdown.fraction = 1.0 - ((current_seconds - start_seconds) / 30.0); | ||||
|  | ||||
|             check_update_totps(); | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         public Window(GAuthenticator.App app) { | ||||
|             Object(application: app); | ||||
|  | ||||
|             // Roughly 1/24 second, for a smooth(ish) update | ||||
|             GLib.Timeout.add(41, update_countdown); | ||||
|  | ||||
|             update_totps(true); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										42
									
								
								src/otp-row.vala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/otp-row.vala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| namespace GAuthenticator { | ||||
|     [GtkTemplate (ui = "/eu/polonkai/gergely/gauthenticator/otp-row.ui")] | ||||
|     class OTPRow : Gtk.Grid { | ||||
|         [GtkChild] | ||||
|         private Gtk.Label code; | ||||
|  | ||||
|         [GtkChild] | ||||
|         private Gtk.Label provider_name; | ||||
|  | ||||
|         [GtkChild] | ||||
|         private Gtk.Label account_name; | ||||
|  | ||||
|         public string secret {get; set;} | ||||
|         public string provider {get; set;} | ||||
|         public string account {get; set;} | ||||
|  | ||||
|         private OTP.TOTP generator; | ||||
|  | ||||
|         public OTPRow(string secret, string provider, string account) { | ||||
|             Object(secret: secret, provider: provider, account: account); | ||||
|  | ||||
|             generator = new OTP.TOTP(secret, 6, GLib.ChecksumType.SHA1, 30); | ||||
|  | ||||
|             provider_name.set_text(provider); | ||||
|             account_name.set_text(account); | ||||
|         } | ||||
|  | ||||
|         public void update(bool first = false) { | ||||
|             uint64 val; | ||||
|  | ||||
|             // FIXME: The first run is OK, but the rest is behind one cycle | ||||
|             if (first) { | ||||
|                 val = generator.now(); | ||||
|             } else { | ||||
|                 var dt = new GLib.DateTime.now_utc(); | ||||
|                 val = generator.at(dt, 1); | ||||
|             } | ||||
|  | ||||
|             code.set_text("%06lu".printf((ulong)val)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										72
									
								
								src/otp.vala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/otp.vala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| namespace OTP { | ||||
|     abstract class Base : Object { | ||||
|         private GLib.Bytes _secret; | ||||
|  | ||||
|         public string secret { | ||||
|             get { | ||||
|                 // TODO: Terminate it with a zero! | ||||
|                 return (string)_secret.get_data(); | ||||
|             } | ||||
|  | ||||
|             set { | ||||
|                 _secret = Base32.b32decode(new GLib.Bytes(value.data), true, 0); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public int digits {get; set; default = 6;} | ||||
|         public GLib.ChecksumType digest {get; set; default = GLib.ChecksumType.SHA1;} | ||||
|  | ||||
|         public Base(string secret_key, int digits, GLib.ChecksumType digest) { | ||||
|             Object(secret: secret_key, digits: digits, digest: digest); | ||||
|         } | ||||
|  | ||||
|         private static GLib.Bytes int_to_bytestring(uint64 val) { | ||||
|             var size = (int) sizeof(uint64); | ||||
|             var result = new uint8[size]; | ||||
|             for (int i = size - 1; i >= 0; i--) { | ||||
|                 result[i] = (uint8)(val & 0xFF); | ||||
|                 val >>= 8; | ||||
|             } | ||||
|  | ||||
|             return new GLib.Bytes(result); | ||||
|         } | ||||
|  | ||||
|         public uint64 generate_otp(uint64 input) { | ||||
|             // TODO: Make this dynamic based on the digest | ||||
|             uint8[256] result = {0}; | ||||
|             size_t result_len = 256; | ||||
|  | ||||
|             var hasher = new GLib.Hmac(digest, _secret.get_data()); | ||||
|             hasher.update(int_to_bytestring(input).get_data()); | ||||
|             hasher.get_digest(result, ref result_len); | ||||
|  | ||||
|             var offset = result[result_len - 1] & 0xf; | ||||
|             uint64 code = ((uint64)(result[offset] & 0x7f) << 24 | | ||||
|                            (uint64)(result[offset + 1] & 0xff) << 16 | | ||||
|                            (uint64)(result[offset + 2] & 0xff) << 8 | | ||||
|                            (uint64)(result[offset + 3] & 0xff)); | ||||
|             return Math.llrint(code % GLib.Math.pow(10, digits)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class TOTP : Base { | ||||
|         public int interval {get; set; default = 30;} | ||||
|  | ||||
|         public TOTP(string secret_key, int digits, GLib.ChecksumType digest, int interval) { | ||||
|             Object(secret: secret_key, digits: digits, digest: digest); | ||||
|         } | ||||
|  | ||||
|         public uint64 at(GLib.DateTime for_time, int counter_offset) { | ||||
|             return generate_otp(timecode(for_time) + counter_offset); | ||||
|         } | ||||
|  | ||||
|         public uint64 now() { | ||||
|             return generate_otp(timecode(new GLib.DateTime.now_utc())); | ||||
|         } | ||||
|  | ||||
|         public uint64 timecode(GLib.DateTime for_time) { | ||||
|             return Math.llrint(Math.ceil(for_time.to_unix() / interval)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user