Initial working version

This commit is contained in:
Gergely Polonkai 2017-03-29 12:33:01 +02:00
parent b8c6d15d7e
commit 3c845d4edb
7 changed files with 409 additions and 10 deletions

View File

@ -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
View 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>

View File

@ -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
View 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);
}
}

View File

@ -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
View 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
View 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));
}
}
}