Created
May 20, 2026 06:49
-
-
Save daaku/00f5f10e6c7282a0577b8484e857c07a to your computer and use it in GitHub Desktop.
Fix BitWarden Passkeys imported into KeePassXC for compatibility with KeePassium
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "encoding/pem" | |
| "fmt" | |
| "os" | |
| "strings" | |
| "syscall" | |
| "github.com/daaku/serr" | |
| "github.com/tobischo/gokeepasslib/v3" | |
| "golang.org/x/term" | |
| ) | |
| func fixPEM(input string) string { | |
| // If it already decodes cleanly, return as-is | |
| if block, _ := pem.Decode([]byte(input)); block != nil { | |
| return input | |
| } | |
| // Extract header, footer, and body | |
| headerEnd := strings.Index(input[5:], "-----") + 5 + 5 | |
| header := input[:headerEnd] | |
| footerStart := strings.LastIndex(input, "-----END") | |
| footer := input[footerStart:] | |
| body := input[headerEnd:footerStart] | |
| // Split body into 64-character chunks | |
| var chunks []string | |
| for len(body) > 0 { | |
| chunkSize := min(len(body), 64) | |
| chunks = append(chunks, body[:chunkSize]) | |
| body = body[chunkSize:] | |
| } | |
| // Reassemble with proper newlines | |
| var sb strings.Builder | |
| sb.WriteString(header) | |
| sb.WriteString("\n") | |
| for _, chunk := range chunks { | |
| sb.WriteString(chunk) | |
| sb.WriteString("\n") | |
| } | |
| sb.WriteString(footer) | |
| sb.WriteString("\n") | |
| return sb.String() | |
| } | |
| func updateEntries(entries []gokeepasslib.Entry) error { | |
| for _, e := range entries { | |
| pem := e.Get("KPEX_PASSKEY_PRIVATE_KEY_PEM") | |
| if pem != nil { | |
| fixed := fixPEM(pem.Value.Content) | |
| if fixed == pem.Value.Content { | |
| fmt.Println("OK:", e.GetTitle()) | |
| } else { | |
| pem.Value.Content = fixed | |
| fmt.Println("FIXED:", e.GetTitle()) | |
| } | |
| } | |
| } | |
| return nil | |
| } | |
| func updateGroups(groups []gokeepasslib.Group) error { | |
| for _, g := range groups { | |
| if err := updateEntries(g.Entries); err != nil { | |
| return err | |
| } | |
| if err := updateGroups(g.Groups); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| func run() error { | |
| file, err := os.Open(os.Args[1]) | |
| if err != nil { | |
| return serr.Wrap(err) | |
| } | |
| fmt.Print("Enter password: ") | |
| password, err := term.ReadPassword(int(syscall.Stdin)) | |
| if err != nil { | |
| return serr.Wrap(err) | |
| } | |
| fmt.Println() // move to next line after prompt | |
| db := gokeepasslib.NewDatabase() | |
| db.Credentials = gokeepasslib.NewPasswordCredentials(string(password)) | |
| if err := gokeepasslib.NewDecoder(file).Decode(db); err != nil { | |
| return serr.Wrap(err) | |
| } | |
| if err := db.UnlockProtectedEntries(); err != nil { | |
| return serr.Wrap(err) | |
| } | |
| if err := updateGroups(db.Content.Root.Groups); err != nil { | |
| return err | |
| } | |
| if err := db.LockProtectedEntries(); err != nil { | |
| return serr.Wrap(err) | |
| } | |
| writeFile, err := os.Create(os.Args[2]) | |
| if err != nil { | |
| return serr.Wrap(err) | |
| } | |
| keepassEncoder := gokeepasslib.NewEncoder(writeFile) | |
| if err := keepassEncoder.Encode(db); err != nil { | |
| return serr.Wrap(err) | |
| } | |
| if err := writeFile.Close(); err != nil { | |
| return serr.Wrap(err) | |
| } | |
| return nil | |
| } | |
| func main() { | |
| if err := run(); err != nil { | |
| fmt.Fprintf(os.Stderr, "%+v\n", err) | |
| os.Exit(1) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment