parent
b8c6d15d7e
commit
3c845d4edb
7 changed files with 410 additions and 11 deletions
@ -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> |
@ -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; |
||||
|
||||
public Window(GAuthenticator.App app) { |
||||
Object(application: app); |
||||
} |
||||
} |
||||
[GtkTemplate (ui = "/eu/polonkai/gergely/gauthenticator/gauth-window.ui")] |
||||
class Window : Gtk.ApplicationWindow { |
||||
[GtkChild] |
||||
private Gtk.ProgressBar countdown; |
||||
|
||||
[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); |
||||
} |
||||
} |
||||
} |
||||
|
@ -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)); |
||||
} |
||||
} |
||||
} |
@ -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