8000 Add support for 2FAS by elliotwutingfeng · Pull Request #2 · tjblackheart/andcli · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add support for 2FAS #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ View, decrypt and copy 2FA tokens from encrypted backup files directly in your s

* [andotp](https://github.com/andOTP/andOTP)
* [aegis](https://getaegis.app)
* [twofas](https://2fas.com)

![Demo](doc/demo.gif "Demo")

Expand All @@ -16,7 +17,7 @@ Download a [prebuild release](https://github.com/tjblackheart/andcli/releases) a
## Usage

1. Export an **encrypted, password protected** backup from your 2FA app and save it into your preferred cloud provider (i.e. Dropbox, Nextcloud...).
2. Fire up `andcli` and point it to this file with `-f <path-to-file>`. Specify the vault type via `-t <type>`: choose between `andotp` or `aegis`. The path and type will get cached, so you have to do this only once.
2. Fire up `andcli` and point it to this file with `-f <path-to-file>`. Specify the vault type via `-t <type>`: choose between `andotp` or `aegis` or `twofas`. The path and type will get cached, so you have to do this only once.
3. Enter the encryption password.
4. To search an entry, type a word. Press `ESC` to clear the current query.
5. Navigate via keyboard, press `Enter` to view a token and press `c` to copy it into the clipboard (**Linux/Mac only**).
Expand All @@ -35,7 +36,7 @@ Usage of andcli:
-f string
Path to the encrypted vault
-t string
Vault type (andotp, aegis)
Vault type (andotp, aegis, twofas)
-c string
Clipboard command (by default is the first of `xclip`, `wl-copy` or `pbcopy` found)
-v Show current version
Expand Down
88 changes: 88 additions & 0 deletions cmd/andcli/andcli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,36 @@ func TestDecrypt(t *testing.T) {
}
}

func TestTwoFas(t *testing.T) {
tests := []struct {
name string
filename string
password string
fails bool
}{
{"decrypts", "testdata/twofas-export-test.2fas", "andcli-test", false},
{"fails: wrong password", "testdata/twofas-export-test.2fas", "invalid", true},
{"fails: invalid file", "testdata/twofas-invalid-file.2fas", "invalid", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b, err := os.ReadFile(tt.filename)
assert.NoError(t, err)

entries, err := decryptTWOFAS(b, []byte(tt.password))
if tt.fails {
assert.Error(t, err)
return
}

assert.NoError(t, err)
assert.Len(t, entries, 1)
assert.Equal(t, entries[0].Label, "andcli-test")
})
}
}

func TestAEGIS(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -159,6 +189,64 @@ func TestConvertAEGISEntry(t *testing.T) {
assert.Equal(t, have.toEntry(), want)
}

func TestConvertTwoFasEntry(t *testing.T) {
have := twofasEntry{
Name: "name",
Secret: "secret",
UpdatedAt: 1707198293593,
Otp: struct {
Label string
Account string
Issuer string
Digits int
Period int
Algorithm string
TokenType string `json:"tokenType"`
Source string
}{
Label: "name",
Account: "andcli-test",
Issuer: "issuer",
Digits: 6,
Period: 30,
Algorithm: "algo",
TokenType: "type",
Source: "Link",
},
Order: struct {
Position int
}{Position: 0},
Icon: struct {
Selected string
Label struct {
Text string
BackgroundColor string `json:"backgroundColor"`
}
IconCollection struct {
Id string
} `json:"iconCollection"`
}{
Selected: "Label", Label: struct {
Text string
BackgroundColor string `json:"backgroundColor"`
}{Text: "OT", BackgroundColor: "Orange"}, IconCollection: struct {
Id string
}{Id: "a5b3fb65-4ec5-43e6-8ec1-49e24ca9e7ad"}},
}

want := &entry{
Secret: "secret",
Issuer: "issuer",
Label: "name",
Digits: 6,
Type: "type",
Algorithm: "algo",
Period: 30,
}

assert.Equal(t, have.toEntry(), want)
}

func TestConfig(t *testing.T) {
cwd, err := os.Getwd()
assert.NoError(t, err)
Expand Down
5 changes: 4 additions & 1 deletion cmd/andcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
VIEW_DETAIL = "detail"
TYPE_ANDOTP = "andotp"
TYPE_AEGIS = "aegis"
TYPE_TWOFAS = "twofas"
)

var (
Expand Down Expand Up @@ -63,7 +64,7 @@ func main() {
var showVersion bool

flag.StringVar(&vaultFile, "f", "", "Path to the encrypted vault")
flag.StringVar(&vaultType, "t", "", "Vault type (andotp, aegis)")
flag.StringVar(&vaultType, "t", "", "Vault type (andotp, aegis, twofas)")
flag.StringVar(&clipboardCmd, "c", "", "Clipboard command (xclip, wl-copy, pbcopy, etc.)")
flag.BoolVar(&showVersion, "v", false, "Show current version")
flag.Parse()
Expand Down Expand Up @@ -148,6 +149,8 @@ func decrypt(vaultFile, vaultType string, p ...[]byte) (entries, error) {
return decryptANDOTP(b, pass)
case TYPE_AEGIS:
return decryptAEGIS(b, pass)
case TYPE_TWOFAS:
return decryptTWOFAS(b, pass)
}

return nil, fmt.Errorf("vault type %q: not implemented", vaultType)
Expand Down
1 change: 1 addition & 0 deletions cmd/andcli/testdata/twofas-export-test.2fas
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"services":[],"groups":[],"updatedAt":1707198500794,"schemaVersion":4,"appVersionCode":5000012,"appVersionName":"5.2.0","appOrigin":"android","servicesEncrypted":"ejHkcIU7/KtjZkdm0mx+X+BY/jWAO1lbZaLpiSfADFsEFOo+rUhbaZ+i0a/0DKg5fBooJBqD4h8kJ20lHvkv2gCU30cy/hEs6vBZWTPBwr8dfC07NStxgWY3K78NBi5rhkXfe7QdHxMzhIXsnGOBqp1ibL5INCPESw9BlXQ1OWox/MNbIl9wRg0gRSyag+AF5xWdZ/AqpT/Gx4WM9bMw9MbM7zNKLGEHrlqf3ESO5r2JvPpB/Y2XiM87nCRrpCfQYsWLUwfKG+KkvokuXwwfH9lh8H0MZWqzIYHO8EW1rrBeW6arbjnInAjI4u71n0/MIRyiA+t6RVaGlqMXztAR4yo4Ts1e2RwBWA2TGOWMPoXXzj+uxjSDHmRv1/zDvkcg9FiP0xH2Ftr/fZYYwtUcsX4X5L6Jg+nvLR0wN6Al/zl3yHjgLn0vPMO9YMqtWFo2mnLuBHa8Epaey6ZCLQ6HkT4YHj3H7wQcRrc06Wy+vYs7nNHO5lD9wY+lpdxJGMeXv7nYSkTmSQwfIBSeuhngDZfMqRPZKqF74pjmb2Z/6pIP2ZdiKgceswN7an+ZXVxvDeS3jiS//cQyA9jJrkJB3tk=:w3uke3TsIcM0v0IWyIFNZTZTBkPVoKvckvikTEq/CeKPlUgmICJdhlSdMUuI4m9UO3jUqoAv5uCeLhn0XN5JycEklpd2p9rJUc5aSj3uhDb+Ki1oWBG6K5ePX82NMxp/xyWFZMWQrUbkXxSOXckV06F2ohrpz6hp2DpVgc+WkLecLQ4h2PzO9wEPrEKl+5B/iNnzI+2WXU8CW9oSg4B3JyFI/UVEk/80jMFs2lYFMAV7EmxbOBMuQrf2H/bzBPwYUCAbtqN9nfx54ywXfaHQHX/p11HbmdWgyp2PNAxFi2VqoY/c1TG0OSqs2RVbwYMtAMIGhnQWIcvtMJp4FV1/2A==:Ow6fbkwu/65x56U3","reference":""}
1 change: 1 addition & 0 deletions cmd/andcli/testdata/twofas-invalid-file.2fas
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"invalid"
171 changes: 171 additions & 0 deletions cmd/andcli/twofas.go
9E81
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package main

import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strings"

"golang.org/x/crypto/pbkdf2"
)

const numFields int = 3
const authTagLength int = 16

type (
twofasVault struct {
UpdatedAt int
SchemaVersion int
AppVersionCode int
AppVersionName string
AppOrigin string
ServicesEncrypted string
}

twofasDB []twofasEntry

twofasEntry struct {
Name string
Secret string
UpdatedAt int
Otp struct {
Label string
Account string
Issuer string
Digits int
Period int
Algorithm string
TokenType string `json:"tokenType"`
Source string
}
Order struct {
Position int
}
Icon struct {
Selected string
Label struct {
Text string
BackgroundColor string `json:"backgroundColor"`
}
IconCollection struct {
Id string
} `json:"iconCollection"`
}
}
)

func (e twofasEntry) toEntry() *entry {
return &entry{
Secret: e.Secret,
Issuer: e.Otp.Issuer,
Label: e.Otp.Label,
Digits: e.Otp.Digits,
Type: e.Otp.TokenType,
Algorithm: e.Otp.Algorithm,
Period: e.Otp.Period,
}
}

//

func decryptTWOFAS(data, password []byte) (entries, error) {

var vault twofasVault
if err := json.Unmarshal(data, &vault); err != nil {
return nil, err
}

key, err := deriveTwoFasMasterKey(&vault, password)
if err != nil {
return nil, err
}

plain, err := decryptTwoFasDB(&vault, key)
if err != nil {
return nil, err
}

var db twofasDB
if err := json.Unmarshal(plain, &db); err != nil {
return nil, err
}

var list entries
for _, e := range db {
list = append(list, *e.toEntry())
}

return list, nil
}

func deriveTwoFasMasterKey(v *twofasVault, password []byte) ([]byte, error) {
servicesEncrypted := strings.SplitN(v.ServicesEncrypted, ":", numFields+1)
if len(servicesEncrypted) != numFields {
return nil, fmt.Errorf("Invalid vault file. Number of fields is not %d", numFields)
}
var dbAndAuthTag, salt []byte
var err error

dbAndAuthTag, err = base64.StdEncoding.DecodeString(servicesEncrypted[0])
if err != nil {
return nil, err
}
salt, err = base64.StdEncoding.DecodeString(servicesEncrypted[1])
if err != nil {
return nil, err
}

if len(dbAndAuthTag) <= authTagLength {
return nil, fmt.Errorf("Invalid vault file. Length of cipher text with auth tag must be more than %d", authTagLength)
}

return pbkdf2.Key(password, salt, 10000, 32, sha256.New), nil
}

func decryptTwoFasDB(v *twofasVault, key []byte) ([]byte, error) {

servicesEncrypted := strings.SplitN(v.ServicesEncrypted, ":", numFields+1)
if len(servicesEncrypted) != numFields {
return nil, fmt.Errorf("Invalid vault file. Number of fields is not %d", numFields)
}

var dbAndAuthTag, b, tag, nonce []byte
var err error

dbAndAuthTag, err = base64.StdEncoding.DecodeString(servicesEncrypted[0])
if err != nil {
return nil, err
}

b = dbAndAuthTag[:len(dbAndAuthTag)-authTagLength]
tag = dbAndAuthTag[len(dbAndAuthTag)-authTagLength:]

nonce, err = base64.StdEncoding.DecodeString(servicesEncrypted[2])
if err != nil {
return nil, err
}

block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

var c []byte
c = append(c, b...)
c = append(c, tag...)

plain, err := gcm.Open(nil, nonce, c, nil)
if err != nil {
return nil, err
}

return plain, nil
}
Loading
0