otpauth: decoding Google Authenticator migration links
Google Authenticator migration decoder
At a glance
- What is it?
- otpauth is a Go command line tool that takes the otpauth-migration link produced by Google Authenticator's export function and turns it into ordinary otpauth links, QR codes or a small local web view. It is a migration aid, and the secrets it handles are worth thinking about before you point it at anything.
- Who is it for?
- otpauth is a good fit for one specific job: moving your own two-factor accounts out of Google Authenticator into another authenticator, on a machine you control and can wipe afterwards. It is a poor fit for shared machines, for a team that wants a secrets vault, and for anything that requires the migration link to stay encrypted, since the export is plain base64 in a URL.
- Can I use it commercially?
- Yes. ISC is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 85 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What a Google Authenticator export actually is
Moving away from Google Authenticator used to mean re-enrolling every account by hand. The escape hatch is the transfer link. In the app, the path is the three-dot menu, then Transfer accounts, then Export accounts, which produces a QR code. That QR code encodes an otpauth-migration URL of the form otpauth-migration://offline?data=..., and the data parameter holds the accounts in a serialised form.
The otpauth tool exists to read that payload. Its job, as the README puts it, is converting Google Authenticator transfer links into plain otpauth links, the Key URI Format that other authenticator apps understand. The output of a successful decode looks like this:
otpauth://totp/Example:[email protected]?issuer=Example&secret=JBSWY3DPEHPK3PXPOne line per account, in the format you would type into another app by hand, which is exactly what makes bulk migration bearable.
The direction of travel matters for judging the tool. This is not a generator of codes and not a credential store: it is a format converter for a file you exported yourself. The related project linked at the end of the README, dim13/2fa, is a separate small command line evaluator for two-factor codes, and the two are kept apart.
The implementation is Go, with a module path of github.com/dim13/otpauth and a go 1.24 directive. The dependencies are small and purposeful: google.golang.org/protobuf for the migration payload, skip2/go-qrcode for image generation, and google/uuid. The repository layout has main.go, http.go, a migration package, a static directory for the web view and a images directory for the logo.
Installing the tool: go install, a release binary or a container
There are three documented ways to get it, and the first is the normal one for Go developers:
go install github.com/dim13/otpauth@latestThe README also points at the latest binary release on GitHub, which avoids needing a Go toolchain at all. The binary lands in ~/go/bin/otpauth with the go install route, and every example in the documentation uses that path.
A third option is the container image published to ghcr.io. The Dockerfile is a two-stage build: a golang:latest builder stage that copies the source, sets CGO_ENABLED=0 with the target OS and architecture, and runs go build, followed by a scratch stage that copies only the resulting binary and sets it as the entrypoint. The result is an image with no shell and no package manager in it, which is the right shape for a container that handles secrets.
The documented run command mounts a working directory and publishes the HTTP port:
docker run --rm -p 6060:6060 -v $(pwd)/workdir:/app/workdir \
ghcr.io/dim13/otpauth:latest \
-workdir /app/workdir -http :6060 \
-link "otpauth-migration://offline?data=CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC"The --rm flag matters here, since the container writes its output to the mounted directory and the volume is what survives. The README spells out the two pieces of the command: the port mapping sends host 6060 to container 6060, and the volume maps the host directory to the container directory. In the example the container listens on all interfaces on port 6060 while the publish flag maps the same port on the host, so on a shared network that service is reachable from other machines. Binding to :6060 in the container is not the same as binding to localhost on the host, and the difference is worth keeping in mind before running this on a laptop connected to an office network.
The flag set and what each mode does with the secrets
The tool is a single binary with a set of flags, and the interesting ones are about how much you want the program to do with the decoded data.
The first group controls output. The -dump flag dumps the payload as prototext, which is the protobuf text format, and is the way to see the raw structure of an export including fields the otpauth link does not carry. The -info flag displays batch information. The -qr flag generates QR codes in the otpauth format, which you then scan into the new authenticator:
-dump
dump as prototext
-eval
evaluate otps
-http string
serve http (e.g. localhost:6060)
-info
display batch infoThe second group covers the input and the output location. The -link flag takes the migration link and is required. The -rev flag runs the conversion in the opposite direction, producing an otpauth-migration link from otpauth links, which is what the name suggests. The -workdir flag sets the working directory that generated files are written into.
-link string
migration link (required)
-qr
generate QR-codes (optauth://)
-rev
reverse QR-code (otpauth-migration://)
-workdir string
working directoryThe -eval flag is the one to think hardest about. It evaluates the one-time passwords in the batch, which means it computes live codes from the shared secrets. It is a debugging and verification aid, useful for checking that a migrated account really works. It is also the flag that turns the tool from a converter into a code generator, and anyone who can run it against an export can produce valid codes for those accounts.
The -http flag serves the decoded data over HTTP. Running it with the example address and then opening the page is the documented workflow:
~/go/bin/otpauth -http=localhost:6060 -link "otpauth-migration://offline?data=CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC"The HTTP mode is a convenience for people who would rather pick accounts in a browser than read a terminal, and it is also the mode with the largest blast radius, because a web server serving shared secrets is a different proposition from a program that writes files to your home directory.
The security reality of holding every 2FA secret at once
An export from Google Authenticator contains the shared secret for every account in the app. That data is not encrypted in any way that the tool would have to break: it is a base64 payload sitting in a URL. Whoever holds the link holds the ability to generate valid one-time passwords for all of those accounts, and therefore, in most cases, to authenticate as you.
That single fact governs everything else about using this tool. The link should be treated exactly the way you would treat a password file: pasted into a shell on a machine you control, and not pasted into a chat, a ticket, a paste site or a shell history you do not manage. Since the tool takes the link as a command line argument, it will end up in your shell history, which is why the -workdir flag and a scratch machine both exist as options.
The generated artefacts need the same care. The -qr mode writes PNG files into the working directory, one image per account, each of which is a scannable copy of a secret. The README even notes that you should view and scan the png files in the current working directory, which means the files sit there until you delete them. The HTTP mode puts the same secrets behind a port, and the Docker example publishes that port to the host.
None of this is a flaw in the tool, since the tool is doing a job that inherently requires holding the secrets. It is a property of the job. The mitigations are practical: run the conversion on a machine you can wipe, delete the work directory and the PNG files afterwards, keep the HTTP mode on localhost, and re-enrol the accounts in the new authenticator as soon as the transfer is verified.
The README does not document any encryption of the payload, any keychain storage, or any audit of the files it writes, and it does not claim one. Nor does it document a mechanism to invalidate the old export, which is why changing the secrets or removing the accounts from the old app after the migration is the sensible final step.
The Go implementation: protobuf decoding and a static binary
The choice of Go and protobuf is not incidental. The migration payload is a protobuf message, and the repository declares google.golang.org/protobuf together with a protoc-gen-go tool directive, which means the message definition is compiled rather than parsed by hand. The -dump flag exists precisely to show that structure as prototext, so a user can see fields such as the batch identifier and the per-account entries that the plain otpauth link does not carry.
The v0.6.0 release note names a UniqueId field, which indicates that the schema gained a field the author needed to expose, and it is the kind of change that only makes sense when you can dump the raw message. Anything the tool does not map into an otpauth link is still visible through -dump, which is a useful safety net for a migration where a service-specific field might otherwise be lost.
The build story is equally deliberate. CGO_ENABLED=0 and a scratch base image produce a container with a single static binary and nothing else in it, so there is no shell to drop into and no package manager to install anything with. For a container whose job is to hold secrets briefly, that is the shape you want.
The HTTP server is a separate file, http.go, alongside the static directory that serves the front end. The README does not document a configuration file, an authentication mechanism for the web view, or any setting for the listen address beyond the flag itself, and there is no claim in the documentation that the served page is protected in any way. Treat the port as an open door unless you bind it to loopback and keep the machine offline while you work.
Licence, release cadence and where to look for related tools
The licence is ISC, a short permissive licence that allows use, modification and redistribution with the copyright notice and the licence text preserved, including in commercial use. The LICENSE file at the repository root carries the terms, and this description covers the published licence rather than legal advice.
The release history is short and recent. Version 0.5.4 came out on 2025-03-07, v0.6.0 on 2025-04-18 with the UniqueId field, and v0.6.1 on 2026-07-30. The version stays below 1.0, which is the author's own signal that the tool is not claiming a stable interface. The repository is not archived, and the last push was on 2026-07-30, the same day as the v0.6.1 tag, so the code and the last published binary correspond to each other.
For related work, the README points at dim13/2fa for a simple command line two-factor evaluator, and the upstream issue tracker for Google Authenticator's transfer link, issue 118, is the reference for how the export format came about in the first place. Both are worth reading before you build anything larger on top of this decoder, because the export format is a moving target: Google can change what the transfer link contains, and a decoder that only handles today's shape is a decoder that will break.
The documentation itself is brief. There is no command reference beyond the flag list, no section on what happens with an unsupported payload, and no changelog file at the top level of the repository, so the release notes on the tags are the closest thing to a history you can rely on. For a tool this small that is acceptable, but it means you should pin the binary version rather than track latest.
Editorial conclusion
otpauth is a good fit for one specific job: moving your own two-factor accounts out of Google Authenticator into another authenticator, on a machine you control and can wipe afterwards. It is a poor fit for shared machines, for a team that wants a secrets vault, and for anything that requires the migration link to stay encrypted, since the export is plain base64 in a URL. Before running it, treat the migration link as a password rather than as a link, delete the generated QR images and the work directory when the transfer is done, and bind the HTTP mode to localhost only.
Frequently asked questions
What does the otpauth tool do with a Google Authenticator export?
It converts an otpauth-migration://offline?data=... link into plain otpauth links that other authenticator apps understand, one line per account. The default output includes the issuer and the shared secret of each account.
How do I install otpauth and decode a migration link?
Run go install github.com/dim13/otpauth@latest, or download the latest binary release, then pass the link with the -link flag, for example ~/go/bin/otpauth -link "otpauth-migration://offline?data=...". The tool prints an otpauth:// line for each account.
What do the -dump, -eval, -qr and -rev flags do?
-dump prints the decoded payload as prototext, -eval computes the one-time passwords for the batch, -qr writes scannable QR images in the otpauth format, and -rev converts otpauth links back into an otpauth-migration link.
Is it safe to run the otpauth HTTP mode on a shared network?
The documentation does not describe any authentication for the served view. The published Docker example binds the container to port 6060 on all interfaces and maps the same port on the host, so restricting the listener to localhost and running the tool on an offline machine keeps the decoded secrets off the network.
What licence does otpauth use?
The repository is licensed ISC, a permissive licence that allows use, modification and redistribution provided the copyright notice and licence text are preserved, and it does not restrict commercial use.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/dim13-otpauth)Was this useful?