Skip to content

Commit 3c262a1

Browse files
committed
Updated
1 parent b9f7f0e commit 3c262a1

24 files changed

+975
-232
lines changed

cmd/api/bitwarden.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"github.com/djthorpe/go-tablewriter"
8+
"github.com/mutablelogic/go-client"
9+
"github.com/mutablelogic/go-client/pkg/bitwarden"
10+
"github.com/mutablelogic/go-client/pkg/bitwarden/schema"
11+
12+
// Namespace import
13+
. "github.com/djthorpe/go-errors"
14+
)
15+
16+
///////////////////////////////////////////////////////////////////////////////
17+
// GLOBALS
18+
19+
const (
20+
bwName = "bitwarden"
21+
bwDirPerm = 0700
22+
)
23+
24+
var (
25+
bwClient *bitwarden.Client
26+
bwClientId, bwClientSecret string
27+
bwConfigDir, bwCacheDir string
28+
bwForce bool
29+
)
30+
31+
///////////////////////////////////////////////////////////////////////////////
32+
// LIFECYCLE
33+
34+
func bwRegister(flags *Flags) {
35+
// Register flags required
36+
flags.String(bwName, "bitwarden-client-id", "${BW_CLIENTID}", "Client ID")
37+
flags.String(bwName, "bitwarden-client-secret", "${BW_CLIENTSECRET}", "Client Secret")
38+
flags.Bool(bwName, "force", false, "Force login or sync to Bitwarden, even if existing token or data is valid")
39+
// flags.String(bwName, "bitwarden-device-id", "${BW_DEVICEID}", "Device Identifier")
40+
// flags.String(bwName, "bitwarden-password", "${BW_PASSWORD}", "Master Password")
41+
42+
// Register commands
43+
flags.Register(Cmd{
44+
Name: bwName,
45+
Description: "Interact with the Bitwarden API",
46+
Parse: bwParse,
47+
Fn: []Fn{
48+
{Name: "login", Description: "Login to Bitwarden", Call: bwLogin},
49+
{Name: "sync", Description: "Sync items from Bitwarden", Call: bwSync},
50+
{Name: "folders", Description: "Retrieve folders", Call: bwFolders},
51+
},
52+
})
53+
}
54+
55+
func bwParse(flags *Flags, opts ...client.ClientOpt) error {
56+
if client, err := bitwarden.New(opts...); err != nil {
57+
return err
58+
} else {
59+
bwClient = client
60+
}
61+
62+
// Get config directory
63+
if config, err := os.UserConfigDir(); err != nil {
64+
return err
65+
} else {
66+
bwConfigDir = filepath.Join(config, bwName)
67+
if err := os.MkdirAll(bwConfigDir, bwDirPerm); err != nil {
68+
return err
69+
}
70+
}
71+
// Get cache directory
72+
if cache, err := os.UserCacheDir(); err != nil {
73+
return err
74+
} else {
75+
bwCacheDir = filepath.Join(cache, bwName)
76+
if err := os.MkdirAll(bwCacheDir, bwDirPerm); err != nil {
77+
return err
78+
}
79+
}
80+
81+
// Set client ID and secret
82+
bwClientId = flags.GetString("bitwarden-client-id")
83+
bwClientSecret = flags.GetString("bitwarden-client-secret")
84+
if bwClientId == "" || bwClientSecret == "" {
85+
return ErrBadParameter.With("Missing -bitwarden-client-id or -bitwarden-client-secret argument")
86+
}
87+
88+
// Get the force flag
89+
bwForce = flags.GetBool("force")
90+
91+
// Return success
92+
return nil
93+
}
94+
95+
///////////////////////////////////////////////////////////////////////////////
96+
// API METHODS
97+
98+
func bwLogin(w *tablewriter.TableWriter) error {
99+
// Load session or create a new one
100+
session, err := bwReadSession()
101+
if err != nil {
102+
return err
103+
}
104+
105+
// Login options
106+
opts := []bitwarden.SessionOpt{
107+
bitwarden.OptCredentials(bwClientId, bwClientSecret),
108+
}
109+
if session.Device == nil {
110+
opts = append(opts, bitwarden.OptDevice(bitwarden.Device{
111+
Name: bwName,
112+
}))
113+
}
114+
if bwForce {
115+
opts = append(opts, bitwarden.OptForce())
116+
}
117+
118+
// Perform the login
119+
if err := bwClient.Login(session, opts...); err != nil {
120+
return err
121+
}
122+
123+
// Save session
124+
if err := bwWriteSession(session); err != nil {
125+
return err
126+
}
127+
128+
// Print out session
129+
w.Write(session)
130+
131+
// Return success
132+
return nil
133+
}
134+
135+
func bwSync(w *tablewriter.TableWriter) error {
136+
// Load session or create a new one
137+
session, err := bwReadSession()
138+
if err != nil {
139+
return err
140+
}
141+
// If the session is not valid, then return an error
142+
if !session.IsValid() {
143+
return ErrOutOfOrder.With("Session is not valid, login first")
144+
}
145+
// Perform the sync
146+
sync, err := bwClient.Sync(session)
147+
if err != nil {
148+
return err
149+
} else if err := bwWrite("profile.json", sync.Profile); err != nil {
150+
return err
151+
} else if err := bwWrite("folders.json", sync.Folders); err != nil {
152+
return err
153+
} else if err := bwWrite("ciphers.json", sync.Ciphers); err != nil {
154+
return err
155+
} else if err := bwWrite("domains.json", sync.Domains); err != nil {
156+
return err
157+
}
158+
159+
// Output the profile
160+
w.Write(sync.Profile)
161+
162+
// Return success
163+
return nil
164+
}
165+
166+
func bwFolders(w *tablewriter.TableWriter) error {
167+
// Load session or create a new one
168+
session, err := bwReadSession()
169+
if err != nil {
170+
return err
171+
}
172+
173+
// If the session is not valid, then return an error
174+
if !session.IsValid() {
175+
return ErrOutOfOrder.With("Session is not valid, login first")
176+
}
177+
178+
// Read the folders
179+
folders := schema.Folders{}
180+
if err := bwRead("folders.json", &folders); err != nil {
181+
return err
182+
}
183+
184+
// Decrypt the folders from the session
185+
for i, folder := range folders {
186+
if decrypted, err := folder.Decrypt(session); err != nil {
187+
return err
188+
} else {
189+
folders[i] = decrypted
190+
}
191+
}
192+
193+
// Output the folders
194+
w.Write(folders)
195+
196+
// Return success
197+
return nil
198+
}
199+
200+
///////////////////////////////////////////////////////////////////////////////
201+
// OTHER
202+
203+
func bwReadSession() (*bitwarden.Session, error) {
204+
result := new(bitwarden.Session)
205+
filename := filepath.Join(bwConfigDir, "session.json")
206+
if _, err := os.Stat(filename); os.IsNotExist(err) {
207+
// Return a new, empty session
208+
return result, nil
209+
}
210+
211+
// Open the file
212+
file, err := os.Open(filename)
213+
if err != nil {
214+
return nil, err
215+
}
216+
defer file.Close()
217+
218+
// Read and return the session
219+
return result, result.Read(file)
220+
}
221+
222+
func bwWriteSession(session *bitwarden.Session) error {
223+
return bwWrite("session.json", session)
224+
}
225+
226+
func bwWrite(filename string, obj schema.ReaderWriter) error {
227+
path := filepath.Join(bwConfigDir, filename)
228+
w, err := os.Create(path)
229+
if err != nil {
230+
return err
231+
}
232+
defer w.Close()
233+
234+
// Write the object and return any errors
235+
return obj.Write(w)
236+
}
237+
238+
func bwRead(filename string, obj schema.ReaderWriter) error {
239+
path := filepath.Join(bwConfigDir, filename)
240+
r, err := os.Open(path)
241+
if err != nil {
242+
return err
243+
}
244+
defer r.Close()
245+
246+
// Write the object and return any errors
247+
return obj.Read(r)
248+
}

cmd/api/cmd.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package main
2+
3+
import (
4+
"github.com/djthorpe/go-tablewriter"
5+
"github.com/mutablelogic/go-client"
6+
)
7+
8+
///////////////////////////////////////////////////////////////////////////////
9+
// TYPES
10+
11+
type Cmd struct {
12+
Name string
13+
Description string
14+
Parse func(*Flags, ...client.ClientOpt) error
15+
Fn []Fn
16+
}
17+
18+
type Fn struct {
19+
Name string
20+
Description string
21+
Call func(*tablewriter.TableWriter) error
22+
}
23+
24+
///////////////////////////////////////////////////////////////////////////////
25+
// PUBLIC METHODS
26+
27+
func (c *Cmd) Get(match string) *Fn {
28+
for _, fn := range c.Fn {
29+
if fn.Name == match {
30+
return &fn
31+
}
32+
}
33+
return nil
34+
}

0 commit comments

Comments
 (0)