Initial working version
This commit is contained in:
parent
b8c6d15d7e
commit
3c845d4edb
@ -2,5 +2,6 @@
|
|||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="/eu/polonkai/gergely/gauthenticator">
|
<gresource prefix="/eu/polonkai/gergely/gauthenticator">
|
||||||
<file preprocess="xml-stripblanks">gauth-window.ui</file>
|
<file preprocess="xml-stripblanks">gauth-window.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">otp-row.ui</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</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
|
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_CPPFLAGS = $(GAUTHENTICATOR_CFLAGS)
|
||||||
gauthenticator_VALAFLAGS = --pkg gtk+-3.0 --gresources $(gresource_file) --target-glib=2.38
|
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
|
-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 {
|
namespace GAuthenticator {
|
||||||
[GtkTemplate (ui = "/eu/polonkai/gergely/gauthenticator/gauth-window.ui")]
|
[GtkTemplate (ui = "/eu/polonkai/gergely/gauthenticator/gauth-window.ui")]
|
||||||
class Window : Gtk.ApplicationWindow {
|
class Window : Gtk.ApplicationWindow {
|
||||||
[GtkChild]
|
[GtkChild]
|
||||||
private Gtk.ProgressBar countdown;
|
private Gtk.ProgressBar countdown;
|
||||||
|
|
||||||
public Window(GAuthenticator.App app) {
|
[GtkChild]
|
||||||
Object(application: app);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user