Skip to content

Commit

Permalink
Merge pull request #22 from rusq/v120-migration
Browse files Browse the repository at this point in the history
Add creds migration and introduce -logout
  • Loading branch information
rusq authored Jan 8, 2025
2 parents 613ca8d + e347cc4 commit 851f8d7
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 11 deletions.
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ You will need:
- Telegram API ID
- Telegram API HASH

The program provides easy to follow instructions on how to get
those.
The program provides easy to follow instructions on how to get those.

To authenticate, you will use your Telegram Account phone number and the code,
that will be sent to you in-app or a text message (SMS).
Expand All @@ -40,17 +39,25 @@ You can also run the deletion from script. Follow these steps:
wipemychat -wipe 12345,56789
```

## Resetting login details
### Logging out

If you accidentally entered the wrong login details, or App Hash and App
Secret, you can reset them by running:
If you need to log in under a different account (or phone number), you can
logout without deleting the application credentials by running:
```
wipemychat -logout
```

### Complete reset

If you need to completely reset the authentication, for example, if you
accidentally entered the wrong login details, or App Hash and App Secret, run:

```
wipemychat -reset
```

This deletes both files with secrets, and you will be asked to authenticate
again.
This deletes both files: session and application credentials. You will be asked
to authenticate again.

## Licence
GNU Public Licence 3.0, see [LICENCE][2]
Expand Down
36 changes: 32 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,12 @@ type Params struct {
ApiHash string
Phone string

// Reset requests removal of the session and API credentials files.
Reset bool
List bool
// Logout requests removal of the session file.
Logout bool

List bool
Batch chatIDs

Version bool
Expand Down Expand Up @@ -110,13 +113,20 @@ func (c *chatIDs) String() string {
func parseCmdLine() (Params, error) {
p := Params{CacheDirName: cacheDirName}
{
// auth options
flag.IntVar(&p.ApiID, "api-id", osenv.Secret("APP_ID", 0), "Telegram API ID")
flag.StringVar(&p.ApiHash, "api-token", osenv.Secret("APP_HASH", ""), "Telegram API token")
flag.StringVar(&p.Phone, "phone", osenv.Value("PHONE", ""), "phone `number` in international format for authentication (optional)")
flag.BoolVar(&p.Reset, "reset", false, "reset authentication")

// reset options
flag.BoolVar(&p.Reset, "reset", false, "reset authentication (logout and remove credentials)")
flag.BoolVar(&p.Logout, "logout", false, "logout current account, use this to login as another user with the same API ID")

// batch mode
flag.BoolVar(&p.List, "list", false, "list channels and their IDs")
flag.Var(&p.Batch, "wipe", "batch mode, specify comma separated chat IDs on the command line")

// sundry
flag.BoolVar(&p.Version, "v", false, "print version and exit")
flag.BoolVar(&p.Verbose, "verbose", osenv.Value("DEBUG", "") != "", "verbose output")
flag.StringVar(&p.Trace, "trace", osenv.Value("TRACE_FILE", ""), "trace `filename`")
Expand Down Expand Up @@ -157,15 +167,33 @@ func run(ctx context.Context, p Params) error {

header(os.Stdout)

sessfile := filepath.Join(p.cacheDir, "session.dat")
if migrated, err := migratev120(sessfile); err != nil {
return err
} else if migrated {
fmt.Fprintln(os.Stdout, "session file was migrated to new format")
}

sessStorage := session.FileStorage{Path: filepath.Join(p.cacheDir, "session.dat")}
apiCredsFile := filepath.Join(p.cacheDir, "telegram.dat")
if p.Logout {
if err := unlink(sessStorage.Path); err != nil {
return err
} else {
fmt.Fprintln(os.Stdout, "you were logged out")
}
os.Exit(0)
}
if p.Reset {
for _, file := range []string{sessStorage.Path, apiCredsFile} {
if err := unlink(file); err != nil {
return err
if os.IsNotExist(err) {
continue
}
return fmt.Errorf("error deleting %s: %w", file, err)
}
}
fmt.Fprintln(os.Stdout, "credentials were removed")
fmt.Fprintln(os.Stdout, "logged out and credentials removed")
os.Exit(0)
}

Expand Down
60 changes: 60 additions & 0 deletions migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"bytes"
"context"
"fmt"
"os"

tds "github.com/gotd/td/session"

"github.com/rusq/wipemychat/internal/session"
)

const v1signature = `{"Version":1`

// migratev120 migrates session file from v1 to v1.2.0+ (enables encryption).
// sessfile is the path to the session file. It returns true if the file was
// migrated, false if it was already migrated or invalid.
func migratev120(sessfile string) (bool, error) {
f, err := os.Open(sessfile)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
defer f.Close()
if f, err := f.Stat(); err != nil {
return false, err
} else if f.Size() == 0 {
return false, nil
}
b := make([]byte, len(v1signature))
if n, err := f.Read(b); err != nil {
return false, fmt.Errorf("failed to read session file: %w", err)
} else if n != len(v1signature) {
return false, fmt.Errorf("invalid session file")
}

if !bytes.Equal(b[:], []byte(v1signature)) {
// already migrated or invalid
return false, nil
}
// needs to be migrated
if err := f.Close(); err != nil {
return false, fmt.Errorf("close error: %w", err)
}
v1loader := tds.FileStorage{Path: sessfile}
sess, err := v1loader.LoadSession(context.Background())
if err != nil {
return false, fmt.Errorf("failed to load session: %w", err)
}

// overwrite with new version
v120loader := session.FileStorage{Path: sessfile}
if err := v120loader.StoreSession(context.Background(), sess); err != nil {
return false, fmt.Errorf("failed to save session: %w", err)
}
return true, nil
}
91 changes: 91 additions & 0 deletions migrate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package main

import (
"bytes"
"context"
"io"
"os"
"path/filepath"
"testing"

"github.com/rusq/wipemychat/internal/session"
)

func copyfile(t *testing.T, src, dst string) error {
t.Helper()
sf, err := os.Open(src)
if err != nil {
t.Fatalf("source: %s", err)
}
defer sf.Close()
df, err := os.Create(dst)
if err != nil {
t.Fatalf("destination: %s", err)
}
defer df.Close()
if _, err := io.Copy(df, sf); err != nil {
t.Fatalf("copy: %s", err)
}
return nil
}

func Test_migratev120(t *testing.T) {
type args struct {
sessfile string
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
{
name: "migrates v1 file",
args: args{sessfile: "testdata/testsessionv100.dat"},
want: true,
},
{
name: "doesn't touch v1.20 file",
args: args{sessfile: "testdata/testsessionv120.dat"},
want: false,
},
{
name: "invalid file",
args: args{sessfile: "testdata/invalidsession.dat"},
want: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// substitute the source file with it's copy in a temporary directory
tmpdir := t.TempDir()
srcfilename := filepath.Base(tt.args.sessfile)
dstfile := filepath.Join(tmpdir, srcfilename)
copyfile(t, tt.args.sessfile, filepath.Join(tmpdir, srcfilename))
migrated, err := migratev120(dstfile)
if (err != nil) != tt.wantErr {
t.Errorf("migratev120() error = %v, wantErr %v", err, tt.wantErr)
return
}
if migrated != tt.want {
t.Errorf("migratev120() = %v, want %v", migrated, tt.want)
}
if migrated {
// verify the file is valid v1.20
sess := session.FileStorage{Path: dstfile}
data, err := sess.LoadSession(context.Background())
if err != nil {
t.Errorf("migratev120() = %v, want %v", err, nil)
}
if data == nil {
t.Errorf("migratev120() = %v, want %v", data, "not nil")
}
sigSz := len(v1signature)
if !bytes.Equal(data[:sigSz], []byte(v1signature)) {
t.Errorf("migratev120() = %v, want %v", data[:sigSz], v1signature)
}
}
})
}
}
1 change: 1 addition & 0 deletions testdata/invalidsession.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Ver"}
1 change: 1 addition & 0 deletions testdata/testsessionv100.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Version":1,"Data":{"Config":{"BlockedMode":false,"ForceTryIpv6":false,"Date":1536300799,"Expires":1536304334,"TestMode":false,"ThisDC":2,"DCOptions":[{"Flags":0,"Ipv6":false,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":1,"IPAddress":"1.1.1.5","Port":443,"Secret":null},{"Flags":16,"Ipv6":false,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":true,"ThisPortOnly":false,"ID":1,"IPAddress":"1.1.1.5","Port":443,"Secret":null},{"Flags":1,"Ipv6":true,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":1,"IPAddress":"2001:0abc:defa:f000:0000:0000:0000:000a","Port":443,"Secret":null},{"Flags":0,"Ipv6":false,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":2,"IPAddress":"1.1.1.4","Port":443,"Secret":null},{"Flags":16,"Ipv6":false,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":true,"ThisPortOnly":false,"ID":2,"IPAddress":"1.1.1.4","Port":443,"Secret":null},{"Flags":2,"Ipv6":false,"MediaOnly":true,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":2,"IPAddress":"1.1.1.2","Port":443,"Secret":null},{"Flags":1,"Ipv6":true,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":2,"IPAddress":"2001:0abc:defa:f000:0000:0000:0000:000a","Port":443,"Secret":null},{"Flags":3,"Ipv6":true,"MediaOnly":true,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":2,"IPAddress":"2001:0abc:def0:f002:0000:0000:0000:000b","Port":443,"Secret":null},{"Flags":0,"Ipv6":false,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":3,"IPAddress":"1.1.1.1","Port":443,"Secret":null},{"Flags":16,"Ipv6":false,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":true,"ThisPortOnly":false,"ID":3,"IPAddress":"1.1.1.1","Port":443,"Secret":null},{"Flags":1,"Ipv6":true,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":3,"IPAddress":"2001:0abc:defd:f003:0000:0000:0000:000a","Port":443,"Secret":null},{"Flags":0,"Ipv6":false,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":4,"IPAddress":"1.4.67.9","Port":443,"Secret":null},{"Flags":16,"Ipv6":false,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":true,"ThisPortOnly":false,"ID":4,"IPAddress":"1.14.17.9","Port":443,"Secret":null},{"Flags":1,"Ipv6":true,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":4,"IPAddress":"2001:0123:4567:f004:0000:0000:0000:000a","Port":443,"Secret":null},{"Flags":2,"Ipv6":false,"MediaOnly":true,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":4,"IPAddress":"1.1.1.9","Port":443,"Secret":null},{"Flags":3,"Ipv6":true,"MediaOnly":true,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":4,"IPAddress":"2001:0012:3456:f004:0000:0000:0000:000b","Port":443,"Secret":null},{"Flags":1,"Ipv6":true,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":5,"IPAddress":"2001:0123:4567:f005:0000:0000:0000:000a","Port":443,"Secret":null},{"Flags":0,"Ipv6":false,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":false,"ThisPortOnly":false,"ID":5,"IPAddress":"1.1.5.19","Port":443,"Secret":null},{"Flags":16,"Ipv6":false,"MediaOnly":false,"TCPObfuscatedOnly":false,"CDN":false,"Static":true,"ThisPortOnly":false,"ID":5,"IPAddress":"1.18.5.17","Port":443,"Secret":null}],"DCTxtDomainName":"abcd.efgh.com","TmpSessions":0,"WebfileDCID":4},"DC":2,"Addr":"","AuthKey":"UypugUhINJghExngTJ5L2fZ/ckBKey75DgpwWSiBROHVZad1UhGFyvmb7aQxO1g6W+8ERG44srIddnkQm8tl+vMtQO1jPtSv+scBCe3CsFpZFPa6Di3QtJ2AcuyY+w6nOMsEKAQK+kwP1dvta3wLcNHViivE/BBJzeUpwYVKG3/4nQuoJ1HnzK8FUq7E/adYx4w4DZGSlx7k33mC+Uazwz5liSn+vniM1qsz/xVwVUfSwe0FHK6+XmfoqIQ/C0Gvtc7TkRrdbYT0NsUIItw2Lza+zAhkwnKRZUIpHbF4hFJ6RnROKQJYew7DxCKos/RedUmYTTou5XabF7OcUZWAoQ==","AuthKeyID":"xNUXXKHzJvM=","Salt":123948397235972349}}
Binary file added testdata/testsessionv120.dat
Binary file not shown.

0 comments on commit 851f8d7

Please sign in to comment.