diff --git a/data/gauthenticator.gresource.xml b/data/gauthenticator.gresource.xml
index b4a064f..ea6b1ee 100644
--- a/data/gauthenticator.gresource.xml
+++ b/data/gauthenticator.gresource.xml
@@ -2,5 +2,6 @@
gauth-window.ui
+ otp-row.ui
diff --git a/data/otp-row.ui b/data/otp-row.ui
new file mode 100644
index 0000000..17a3795
--- /dev/null
+++ b/data/otp-row.ui
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+ True
+ False
+
+
+
+ 0
+ 1
+ 2
+
+
+
+
+
+ 2
+ 0
+ 2
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+ 1
+ 0
+
+
+
+
diff --git a/src/Makefile.am b/src/Makefile.am
index 2c93349..af12b64 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -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 $<
+
diff --git a/src/base32.vala b/src/base32.vala
new file mode 100644
index 0000000..e0825a5
--- /dev/null
+++ b/src/base32.vala
@@ -0,0 +1,175 @@
+namespace Base32 {
+ public errordomain Base32Error {
+ INCORRECT_PADDING,
+ INCORRECT_MAP01;
+ }
+
+ private const string base32_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+ private Array _base32_tab = null;
+ private GLib.HashTable _b32rev = null;
+
+ public GLib.Bytes b32encode(GLib.Bytes input) {
+ if (_base32_tab == null) {
+ _base32_tab = new Array();
+ 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(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);
+ }
+}
diff --git a/src/gauth-window.vala b/src/gauth-window.vala
index 9492ef5..f9c1184 100644
--- a/src/gauth-window.vala
+++ b/src/gauth-window.vala
@@ -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 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);
+ }
+ }
}
diff --git a/src/otp-row.vala b/src/otp-row.vala
new file mode 100644
index 0000000..1a9a463
--- /dev/null
+++ b/src/otp-row.vala
@@ -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));
+ }
+ }
+}
diff --git a/src/otp.vala b/src/otp.vala
new file mode 100644
index 0000000..8efc842
--- /dev/null
+++ b/src/otp.vala
@@ -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));
+ }
+ }
+
+}