Skip to content

Commit 367fa6d

Browse files
authored
Merge pull request #14 from mutablelogic/v1
Added new API command and updates to bitwarden
2 parents 57daf37 + 73f242a commit 367fa6d

34 files changed

+2109
-175
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This repository contains a generic HTTP client which can be adapted to provide:
99
* Debugging capabilities to see the request and response data
1010
* Streaming JSON responses
1111

12-
Documentation: https://pkg.go.dev/github.com/mutablelogic/go-client/pkg/client
12+
API Documentation: https://pkg.go.dev/github.com/mutablelogic/go-client
1313

1414
There are also some example clients which use this library:
1515

cmd/api/bitwarden.go

+363
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/djthorpe/go-tablewriter"
9+
"github.com/mutablelogic/go-client"
10+
"github.com/mutablelogic/go-client/pkg/bitwarden"
11+
"github.com/mutablelogic/go-client/pkg/bitwarden/schema"
12+
"golang.org/x/term"
13+
14+
// Namespace import
15+
. "github.com/djthorpe/go-errors"
16+
)
17+
18+
///////////////////////////////////////////////////////////////////////////////
19+
// TYPES
20+
21+
type bwCipher struct {
22+
Name string
23+
Username string
24+
URI string
25+
Folder string
26+
}
27+
28+
///////////////////////////////////////////////////////////////////////////////
29+
// GLOBALS
30+
31+
const (
32+
bwName = "bitwarden"
33+
bwDirPerm = 0700
34+
)
35+
36+
var (
37+
bwClient *bitwarden.Client
38+
bwClientId, bwClientSecret string
39+
bwPassword string
40+
bwConfigDir string
41+
bwForce bool
42+
)
43+
44+
///////////////////////////////////////////////////////////////////////////////
45+
// LIFECYCLE
46+
47+
func bwRegister(flags *Flags) {
48+
// Register flags required
49+
flags.String(bwName, "bitwarden-client-id", "${BW_CLIENTID}", "Client ID")
50+
flags.String(bwName, "bitwarden-client-secret", "${BW_CLIENTSECRET}", "Client Secret")
51+
flags.String(bwName, "bitwarden-password", "${BW_PASSWORD}", "Master password")
52+
flags.Bool(bwName, "force", false, "Force login or sync to Bitwarden, even if existing token or data is valid")
53+
54+
// Register commands
55+
flags.Register(Cmd{
56+
Name: bwName,
57+
Description: "Interact with the Bitwarden API",
58+
Parse: bwParse,
59+
Fn: []Fn{
60+
{Name: "auth", Description: "Authenticate with Bitwarden", Call: bwAuth},
61+
{Name: "sync", Description: "Sync items from Bitwarden", Call: bwSync},
62+
{Name: "folders", Description: "Retrieve folders", Call: bwFolders},
63+
{Name: "logins", Description: "Retrieve login items", Call: bwLogins, MaxArgs: 1},
64+
{Name: "password", Description: "Print a password to stdout", Call: bwGetPassword, MinArgs: 1, MaxArgs: 1},
65+
},
66+
})
67+
}
68+
69+
func bwParse(flags *Flags, opts ...client.ClientOpt) error {
70+
if client, err := bitwarden.New(opts...); err != nil {
71+
return err
72+
} else {
73+
bwClient = client
74+
}
75+
76+
// Get config directory
77+
if config, err := os.UserConfigDir(); err != nil {
78+
return err
79+
} else {
80+
bwConfigDir = filepath.Join(config, bwName)
81+
if err := os.MkdirAll(bwConfigDir, bwDirPerm); err != nil {
82+
return err
83+
}
84+
}
85+
86+
// Set defaults
87+
bwClientId = flags.GetString("bitwarden-client-id")
88+
bwClientSecret = flags.GetString("bitwarden-client-secret")
89+
if bwClientId == "" || bwClientSecret == "" {
90+
return ErrBadParameter.With("Missing -bitwarden-client-id or -bitwarden-client-secret argument")
91+
}
92+
bwForce = flags.GetBool("force")
93+
bwPassword = flags.GetString("bitwarden-password")
94+
95+
// Return success
96+
return nil
97+
}
98+
99+
///////////////////////////////////////////////////////////////////////////////
100+
// API METHODS
101+
102+
func bwAuth(w *tablewriter.TableWriter, _ []string) error {
103+
// Load session or create a new one
104+
session, err := bwReadSession()
105+
if err != nil {
106+
return err
107+
}
108+
109+
// Login options
110+
opts := []bitwarden.LoginOpt{
111+
bitwarden.OptCredentials(bwClientId, bwClientSecret),
112+
}
113+
if session.Device == nil {
114+
opts = append(opts, bitwarden.OptDevice(schema.Device{
115+
Name: bwName,
116+
}))
117+
}
118+
if bwForce {
119+
opts = append(opts, bitwarden.OptForce())
120+
}
121+
122+
// Perform the login
123+
if err := bwClient.Login(session, opts...); err != nil {
124+
return err
125+
}
126+
127+
// Save session
128+
if err := bwWriteSession(session); err != nil {
129+
return err
130+
}
131+
132+
// Print out session
133+
w.Write(session)
134+
135+
// Return success
136+
return nil
137+
}
138+
139+
func bwSync(w *tablewriter.TableWriter, _ []string) error {
140+
// Load session or create a new one
141+
session, err := bwReadSession()
142+
if err != nil {
143+
return err
144+
}
145+
// If the session is not valid, then return an error
146+
if !session.IsValid() {
147+
return ErrOutOfOrder.With("Session is not valid, login first")
148+
}
149+
// Perform the sync
150+
sync, err := bwClient.Sync(session)
151+
if err != nil {
152+
return err
153+
} else if err := bwWrite("profile.json", sync.Profile); err != nil {
154+
return err
155+
} else if err := bwWrite("folders.json", sync.Folders); err != nil {
156+
return err
157+
} else if err := bwWrite("ciphers.json", sync.Ciphers); err != nil {
158+
return err
159+
} else if err := bwWrite("domains.json", sync.Domains); err != nil {
160+
return err
161+
}
162+
163+
// Output the profile
164+
w.Write(sync.Profile)
165+
166+
// Return success
167+
return nil
168+
}
169+
170+
func bwFolders(w *tablewriter.TableWriter, _ []string) error {
171+
// Load the profile
172+
profile, err := bwReadProfile()
173+
if err != nil {
174+
return err
175+
}
176+
177+
// Load session or create a new one
178+
session, err := bwReadSession()
179+
if err != nil {
180+
return err
181+
}
182+
183+
// Make an encryption key
184+
if bwPassword == "" {
185+
if v, err := bwReadPasswordFromTerminal(); err != nil {
186+
return err
187+
} else {
188+
bwPassword = v
189+
}
190+
}
191+
if err := session.CacheKey(profile.Key, profile.Email, bwPassword); err != nil {
192+
return err
193+
}
194+
195+
// Read the folders
196+
folders := schema.Folders{}
197+
if err := bwRead("folders.json", &folders); err != nil {
198+
return err
199+
}
200+
201+
// Decrypt the folders from the session
202+
for i, folder := range folders {
203+
if decrypted, err := folder.Decrypt(session); err != nil {
204+
return err
205+
} else {
206+
folders[i] = decrypted.(*schema.Folder)
207+
}
208+
}
209+
210+
// Output the folders
211+
w.Write(folders)
212+
213+
// Return success
214+
return nil
215+
}
216+
217+
func bwLogins(w *tablewriter.TableWriter, _ []string) error {
218+
// Load the profile
219+
profile, err := bwReadProfile()
220+
if err != nil {
221+
return err
222+
}
223+
224+
// Load session or create a new one
225+
session, err := bwReadSession()
226+
if err != nil {
227+
return err
228+
}
229+
230+
// Make an encryption key
231+
if bwPassword == "" {
232+
if v, err := bwReadPasswordFromTerminal(); err != nil {
233+
return err
234+
} else {
235+
bwPassword = v
236+
}
237+
}
238+
if err := session.CacheKey(profile.Key, profile.Email, bwPassword); err != nil {
239+
return err
240+
}
241+
242+
// Read the ciphers
243+
ciphers := schema.Ciphers{}
244+
result := []bwCipher{}
245+
if err := bwRead("ciphers.json", &ciphers); err != nil {
246+
return err
247+
}
248+
// Decrypt the ciphers from the session
249+
for _, cipher := range ciphers {
250+
if cipher.Type != schema.CipherTypeLogin {
251+
continue
252+
}
253+
if decrypted, err := cipher.Decrypt(session); err != nil {
254+
return err
255+
} else {
256+
result = append(result, bwCipher{
257+
Name: decrypted.(*schema.Cipher).Name,
258+
Username: decrypted.(*schema.Cipher).Login.Username,
259+
URI: decrypted.(*schema.Cipher).Login.URI,
260+
Folder: decrypted.(*schema.Cipher).FolderId,
261+
})
262+
}
263+
}
264+
265+
// Output the ciphers
266+
w.Write(result)
267+
268+
// Return success
269+
return nil
270+
}
271+
272+
func bwGetPassword(w *tablewriter.TableWriter, _ []string) error {
273+
return ErrNotImplemented
274+
}
275+
276+
///////////////////////////////////////////////////////////////////////////////
277+
// OTHER
278+
279+
func bwReadPasswordFromTerminal() (string, error) {
280+
stdin := int(os.Stdin.Fd())
281+
if !term.IsTerminal(stdin) {
282+
return "", ErrBadParameter.With("No password set and not running in terminal")
283+
}
284+
fmt.Fprintf(os.Stdout, "Enter password: ")
285+
defer func() {
286+
fmt.Fprintf(os.Stdout, "\n")
287+
}()
288+
if value, err := term.ReadPassword(stdin); err != nil {
289+
return "", err
290+
} else {
291+
return string(value), nil
292+
}
293+
}
294+
295+
func bwReadProfile() (*schema.Profile, error) {
296+
result := schema.NewProfile()
297+
filename := filepath.Join(bwConfigDir, "profile.json")
298+
if _, err := os.Stat(filename); os.IsNotExist(err) {
299+
// Return an error
300+
return nil, ErrNotFound.With("Profile not found")
301+
} else if err != nil {
302+
return nil, err
303+
}
304+
305+
// Open the file
306+
file, err := os.Open(filename)
307+
if err != nil {
308+
return nil, err
309+
}
310+
defer file.Close()
311+
312+
// Read and return the session
313+
return result, result.Read(file)
314+
}
315+
316+
func bwReadSession() (*schema.Session, error) {
317+
result := schema.NewSession()
318+
filename := filepath.Join(bwConfigDir, "session.json")
319+
if _, err := os.Stat(filename); os.IsNotExist(err) {
320+
// Return a new, empty session
321+
return result, nil
322+
} else if err != nil {
323+
return nil, err
324+
}
325+
326+
// Open the file
327+
file, err := os.Open(filename)
328+
if err != nil {
329+
return nil, err
330+
}
331+
defer file.Close()
332+
333+
// Read and return the session
334+
return result, result.Read(file)
335+
}
336+
337+
func bwWriteSession(session *schema.Session) error {
338+
return bwWrite("session.json", session)
339+
}
340+
341+
func bwWrite(filename string, obj schema.ReaderWriter) error {
342+
path := filepath.Join(bwConfigDir, filename)
343+
w, err := os.Create(path)
344+
if err != nil {
345+
return err
346+
}
347+
defer w.Close()
348+
349+
// Write the object and return any errors
350+
return obj.Write(w)
351+
}
352+
353+
func bwRead(filename string, obj schema.ReaderWriter) error {
354+
path := filepath.Join(bwConfigDir, filename)
355+
r, err := os.Open(path)
356+
if err != nil {
357+
return err
358+
}
359+
defer r.Close()
360+
361+
// Write the object and return any errors
362+
return obj.Read(r)
363+
}

0 commit comments

Comments
 (0)