Skip to content

Commit

Permalink
Initial commit, seems to do what it needs to do
Browse files Browse the repository at this point in the history
  • Loading branch information
dereulenspiegel committed Sep 28, 2023
1 parent 6755143 commit e486609
Show file tree
Hide file tree
Showing 8 changed files with 517 additions and 0 deletions.
33 changes: 33 additions & 0 deletions andotp/andotp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package andotp

import (
"encoding/json"
"fmt"
"os"
)

type Service struct {
Secret string `json:"secret"`
Issuer string `json:"issuer"`
Label string `json:"label"`
Digits int `json:"digits"`
Type string `json:"type"`
Algorithm string `json:"algorithm"`
Thumbnail string `json:"thumbnail"`
LastUsed uint64 `json:"last_used"`
UsedFrequency int `json:"used_frequency"`
Period int `json:"period"`
Tags []string `json:"tags"`
}

func Import(filepath string) (services []*Service, err error) {
fileBytes, err := os.ReadFile(filepath)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", filepath, err)
}
services = make([]*Service, 0)
if err = json.Unmarshal(fileBytes, &services); err != nil {
return nil, fmt.Errorf("failed to unmarshal file %s: %w", filepath, err)
}
return
}
40 changes: 40 additions & 0 deletions cli/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"encoding/json"
"flag"
"log"
"os"

andotpconverter "github.com/dereulenspiegel/andotp-converter"
"github.com/dereulenspiegel/andotp-converter/andotp"
)

func main() {
flag.Parse()
if len(flag.Args()) != 2 {
log.Fatalf("You need to specify an input and output file")
}
inputFile := flag.Arg(0)
outputFile := flag.Arg(1)

run(inputFile, outputFile)
}

func run(inputFile, outputFile string) {
andOtpData, err := andotp.Import(inputFile)
if err != nil {
log.Fatalf("failed to load andOTP data: %s", err)
}
twofasData, err := andotpconverter.FromAndOtpTo2Fas(andOtpData)
if err != nil {
log.Fatalf("failed to convert data to 2FAS format: %s", err)
}
twoFasBytes, err := json.Marshal(twofasData)
if err != nil {
log.Fatalf("failed to marshal 2FAS data: %s", err)
}
if err := os.WriteFile(outputFile, twoFasBytes, 0660); err != nil {
log.Fatalf("failed to write 2FAS data to file %s: %s", outputFile, err)
}
}
82 changes: 82 additions & 0 deletions converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package andotpconverter

import (
"time"

"github.com/dereulenspiegel/andotp-converter/andotp"
"github.com/dereulenspiegel/andotp-converter/twofas"
"github.com/google/uuid"
)

func FromAndOtpTo2Fas(andOTPServices []*andotp.Service) (*twofas.RemoteBackup, error) {
updatedAt := uint64(time.Now().UnixMilli())
twofasBackup := &twofas.RemoteBackup{
UpdatedAt: updatedAt,
SchemaVersion: twofas.DEFAULT_SCHEMA_VERSION,
AppVersionCode: twofas.DEFAULT_APP_VERSION_CODE,
AppOrigin: twofas.DEAFULT_APP_ORIGIN,
}

groupMapping := map[string]twofas.RemoteGroup{}
defaultSource := twofas.DEFAULT_SOURCE

for i, andOTPService := range andOTPServices {
var groupId *string
if len(andOTPService.Tags) > 0 {
// Use the first tag as group
tag := andOTPService.Tags[0]
if group, exists := groupMapping[tag]; !exists {
group = twofas.RemoteGroup{
Name: tag,
UpdatedAt: updatedAt,
IsExpanded: true,
ID: uuid.Must(uuid.NewRandom()).String(),
}
groupMapping[tag] = group
groupId = &group.ID
} else {
groupId = &group.ID
}
}

name := andOTPService.Issuer
if name == "" {
name = andOTPService.Label
}
remoteService := twofas.RemoteService{
Name: name,
Secret: andOTPService.Secret,
UpdatedAt: andOTPService.LastUsed,
Order: twofas.Order{Position: i},
Type: thumbnailToServiceType(andOTPService.Thumbnail),
GroupId: groupId,
Otp: twofas.Otp{
Label: &andOTPService.Label,
Account: &andOTPService.Label,
Issuer: &andOTPService.Issuer,
Digits: &andOTPService.Digits,
Period: &andOTPService.Period,
Algorithm: &andOTPService.Algorithm,
Counter: &andOTPService.UsedFrequency,
TokenType: &andOTPService.Type,
Source: &defaultSource,
},
}

twofasBackup.Services = append(twofasBackup.Services, &remoteService)
}
for _, group := range groupMapping {
twofasBackup.Groups = append(twofasBackup.Groups, &group)
}
return twofasBackup, nil
}

func thumbnailToServiceType(thumbnail string) *twofas.ServiceType {
for _, st := range twofas.ServiceTypes {
if string(st) == thumbnail {
return &st
}
}
unknown := twofas.ServiceType("Unknwown")
return &unknown
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/dereulenspiegel/andotp-converter

go 1.21.1

require github.com/google/uuid v1.3.1
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
85 changes: 85 additions & 0 deletions twofas/2fas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package twofas

const (
DEFAULT_SCHEMA_VERSION = 3
DEFAULT_APP_VERSION_CODE = 4070000
DEAFULT_APP_VERSION_NAME = "4.7.0"
DEAFULT_APP_ORIGIN = "android"

TOKEN_TYPE_TOTP = "TOTP"

DEFAULT_SOURCE = "Link"
DEFAULT_ICON_COLLECTION = "a5b3fb65-4ec5-43e6-8ec1-49e24ca9e7ad"
)

type Otp struct {
Link *string `json:"link,omitempty"`
Label *string `json:"label,,omitempty"`
Account *string `json:"account,omitempty"`
Issuer *string `json:"issuer,omitempty"`
Digits *int `json:"digits,omitempty"`
Period *int `json:"period,omitempty"`
Algorithm *string `json:"algorith,omitempty"`
Counter *int `json:"counter,omitempty"`
TokenType *string `json:"tokenType,omitempty"`
Source *string `json:"source,omitempty"`
}

type Order struct {
Position int `json:"position"`
}

type Badge struct {
Color Tint `json:"color"`
}

type Icon struct {
Selected string `json:"selected"` // One of Brand, Label, IconCollection
Brand *Brand `json:"brand,omitempty"`
Label *Label `json:"label,omitempty"`
IconCollection *IconCollection `json:"iconCollection,omitempty"`
}

type Brand struct {
ID *string // Is a ServiceType
}

type IconCollection struct {
ID string `json:"id"`
}

type Label struct {
Text string `json:"text"`
Backgroundcolor Tint `json:"backgroundColor"`
}

type RemoteService struct {
Name string `json:"name"`
Secret string `json:"secret"`
UpdatedAt uint64 `json:"updatedAt"`
Type *ServiceType `json:"type,omitempty"` //TODO import enum ServiceType somehow
Otp Otp `json:"otp"`
Order Order `json:"order"`
Badge *Badge `json:"badge,omitempty"`
Icon *Icon `json:"icon,omitempty"`
GroupId *string `json:"groupId,omitempty"`
}

type RemoteGroup struct {
ID string `json:"id"` // Should be a UUIDv4
Name string `json:"name"`
IsExpanded bool `json:"isExpanded"`
UpdatedAt uint64 `json:"updatedAt"`
}

type RemoteBackup struct {
Services []*RemoteService `json:"services"`
UpdatedAt uint64 `json:"updatedAt"`
SchemaVersion int `json:"schemaVersion"` //Currently at 3
AppVersionCode int `json:"appVersionCode"` //TODO find out a valid value
AppOrigin string `json:"appOrigin"` //defaults to android
Groups []*RemoteGroup `json:"groups"`
Account *string `json:"account,omitempty"`
ServicesEncrypted *string `json:"servicesEncrypted,omitempty"`
Reference *string `json:"reference,omitempty"`
}
Loading

0 comments on commit e486609

Please sign in to comment.