1
0
Fork 0
totp/main.go

297 lines
6.9 KiB
Go
Executable File

/*
* Copyright (c) 2021 Arnaud Ysmal. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"encoding/base32"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"flag"
"fmt"
"math"
"os"
"os/user"
"strings"
"time"
"database/sql"
_ "github.com/mattn/go-sqlite3"
"gopkg.in/ini.v1"
)
type entry struct {
Encrypted bool `json:"encrypted"`
Hash string `json:"hash"`
Index int `json:"index"`
Type string `json:"type"`
Issuer string `json:"issuer"`
Secret string `json:"secret"`
Enc string `json:"enc"`
Period int `json:"period"`
DigitsNo int `json:"digits"`
}
func deriveKeyFromPassword(p string, s []byte) ([]byte, []byte) {
var m []byte
prev := []byte{}
for len(m) < 48 {
a := make([]byte, len(prev)+len(p)+len(s))
copy(a, prev)
copy(a[len(prev):], p)
copy(a[len(prev)+len(p):], s)
nprev := md5.Sum(a)
prev = nprev[:]
m = append(m, prev...)
}
return m[:32], m[32:48]
}
func decryptAESCBC(p string, enc string) ([]byte, error) {
var tmp []byte
var c cipher.Block
var m cipher.BlockMode
var err error
if tmp, err = base64.StdEncoding.DecodeString(enc); err != nil {
return []byte{}, err
}
if !bytes.Equal(tmp[:8], []byte{0x53, 0x61, 0x6c, 0x74, 0x65, 0x64, 0x5f, 0x5f}) {
return []byte{}, errors.New("magic not found")
}
key, iv := deriveKeyFromPassword(p, tmp[8:16])
tmp = tmp[16:]
if c, err = aes.NewCipher(key); err != nil {
return []byte{}, err
}
m = cipher.NewCBCDecrypter(c, iv)
m.CryptBlocks(tmp, tmp)
lastval := int(tmp[len(tmp)-1])
if lastval <= 16 {
tmp = tmp[:len(tmp)-lastval]
}
return tmp, nil
}
func getFirefoxSyncv2Path() (string, error) {
var u *user.User
var err error
var f *ini.File
var p string
if u, err = user.Current(); err != nil {
return "", err
}
if f, err = ini.Load(u.HomeDir + "/.mozilla/firefox/profiles.ini"); err != nil {
return "", err
}
p = f.Section("Profile0").Key("Path").String()
return u.HomeDir + "/.mozilla/firefox/" + p + "/storage-sync-v2.sqlite", nil
}
func getEntriesFromFirefoxAuthenticator(fname string, password string) ([]*entry, error) {
var data string
var db *sql.DB
var err error
var entries map[string]*entry
var pwddb string
var s []byte
var i int
if fname == "" {
fname, err = getFirefoxSyncv2Path()
if err != nil {
return nil, err
}
}
if db, err = sql.Open("sqlite3", fname); err != nil {
return nil, err
}
err = db.QueryRow("select data from storage_sync_data where ext_id=\"authenticator@mymindstorm\";").Scan(&data)
db.Close()
if err != nil {
return nil, err
}
if data == "" {
return nil, errors.New("no data found in database")
}
if err = json.Unmarshal([]byte(data), &entries); err != nil {
return nil, err
}
if pwd, ok := entries["key"]; ok {
if password != "" {
keyPwd, err := decryptAESCBC(password, pwd.Enc)
if err != nil {
return nil, err
}
pwddb = fmt.Sprintf("%x", keyPwd)
}
delete(entries, "key")
}
rv := make([]*entry, len(entries))
for _, v := range entries {
if v.Encrypted && pwddb != "" {
if s, err = decryptAESCBC(pwddb, v.Secret); err == nil {
v.Secret = string(s)
v.Encrypted = false
}
}
rv[i] = v
i++
}
return rv, err
}
func (e *entry) ComputeOTP() (string, error) {
var secretBytes []byte
var err error
var secret string
var period int
var digits int
const (
DefaultPeriod = 30
DefaultDigitsNo = 6
)
secret = strings.ToUpper(strings.TrimSpace(e.Secret))
if n := len(secret) % 8; n != 0 {
secret = secret + strings.Repeat("=", 8-n)
}
if secretBytes, err = base32.StdEncoding.DecodeString(secret); err != nil {
return "", errors.New("decoding of secret as base32 failed")
}
period = e.Period
if period == 0 {
period = DefaultPeriod
}
digits = e.DigitsNo
if digits == 0 {
digits = DefaultDigitsNo
}
mac := hmac.New(sha1.New, secretBytes)
binary.Write(mac, binary.BigEndian,
uint64(math.Floor(float64(time.Now().Unix())/float64(period))))
sum := mac.Sum(nil)
offset := sum[len(sum)-1] & 0xf
value := binary.BigEndian.Uint32(sum[offset:offset+4]) & 0x7fffffff
return fmt.Sprintf("%0*d", digits, uint64(value)%uint64(math.Pow10(digits))), nil
}
func getEntriesFromConfigFile() (entries []*entry, err error) {
var u *user.User
var data []byte
var fname string
if u, err = user.Current(); err != nil {
return
}
fname = u.HomeDir + "/.config/totp/config.json"
if err = os.Chmod(fname, 0600); err != nil {
return
}
if data, err = os.ReadFile(fname); err != nil {
return
}
if err = json.Unmarshal([]byte(data), &entries); err != nil {
return
}
return
}
func main() {
var sentry string
var dbf string
var passwd string
var err error
var entries []*entry
var firefox bool
flag.StringVar(&dbf, "d", "", "Database file")
flag.StringVar(&sentry, "e", "", "Select entry")
flag.BoolVar(&firefox, "f", false, "Get tokens from Firefox Authenticator Extension module")
flag.StringVar(&passwd, "p", "", "Database password")
flag.Parse()
if firefox || dbf != "" {
if entries, err = getEntriesFromFirefoxAuthenticator(dbf, passwd); err != nil {
fmt.Printf("%s\n", err)
return
}
} else {
if entries, err = getEntriesFromConfigFile(); err != nil {
fmt.Printf("%s\n", err)
return
}
}
for _, v := range entries {
if v.Encrypted {
continue
}
if sentry == "" {
p, err := v.ComputeOTP()
if err != nil {
fmt.Printf("%s: %s\n", v.Issuer, err)
} else {
fmt.Printf("%s: %s\n", v.Issuer, p)
}
} else if v.Issuer == sentry {
p, err := v.ComputeOTP()
if err != nil {
fmt.Printf("%s\n", err)
} else {
fmt.Printf("%s\n", p)
}
}
}
}