Skip to content

Commit a815c46

Browse files
committed
Updated bitwarden client
1 parent 3c262a1 commit a815c46

File tree

7 files changed

+159
-83
lines changed

7 files changed

+159
-83
lines changed

cmd/api/bitwarden.go

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package main
22

33
import (
4+
"fmt"
45
"os"
56
"path/filepath"
67

78
"github.com/djthorpe/go-tablewriter"
89
"github.com/mutablelogic/go-client"
910
"github.com/mutablelogic/go-client/pkg/bitwarden"
1011
"github.com/mutablelogic/go-client/pkg/bitwarden/schema"
12+
"golang.org/x/term"
1113

1214
// Namespace import
1315
. "github.com/djthorpe/go-errors"
@@ -24,7 +26,8 @@ const (
2426
var (
2527
bwClient *bitwarden.Client
2628
bwClientId, bwClientSecret string
27-
bwConfigDir, bwCacheDir string
29+
bwPassword string
30+
bwConfigDir string
2831
bwForce bool
2932
)
3033

@@ -35,9 +38,8 @@ func bwRegister(flags *Flags) {
3538
// Register flags required
3639
flags.String(bwName, "bitwarden-client-id", "${BW_CLIENTID}", "Client ID")
3740
flags.String(bwName, "bitwarden-client-secret", "${BW_CLIENTSECRET}", "Client Secret")
41+
flags.String(bwName, "bitwarden-password", "${BW_PASSWORD}", "Master password")
3842
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")
4143

4244
// Register commands
4345
flags.Register(Cmd{
@@ -68,25 +70,15 @@ func bwParse(flags *Flags, opts ...client.ClientOpt) error {
6870
return err
6971
}
7072
}
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-
}
8073

81-
// Set client ID and secret
74+
// Set defaults
8275
bwClientId = flags.GetString("bitwarden-client-id")
8376
bwClientSecret = flags.GetString("bitwarden-client-secret")
8477
if bwClientId == "" || bwClientSecret == "" {
8578
return ErrBadParameter.With("Missing -bitwarden-client-id or -bitwarden-client-secret argument")
8679
}
87-
88-
// Get the force flag
8980
bwForce = flags.GetBool("force")
81+
bwPassword = flags.GetString("bitwarden-password")
9082

9183
// Return success
9284
return nil
@@ -103,11 +95,11 @@ func bwLogin(w *tablewriter.TableWriter) error {
10395
}
10496

10597
// Login options
106-
opts := []bitwarden.SessionOpt{
98+
opts := []bitwarden.LoginOpt{
10799
bitwarden.OptCredentials(bwClientId, bwClientSecret),
108100
}
109101
if session.Device == nil {
110-
opts = append(opts, bitwarden.OptDevice(bitwarden.Device{
102+
opts = append(opts, bitwarden.OptDevice(schema.Device{
111103
Name: bwName,
112104
}))
113105
}
@@ -164,15 +156,37 @@ func bwSync(w *tablewriter.TableWriter) error {
164156
}
165157

166158
func bwFolders(w *tablewriter.TableWriter) error {
159+
// Load the profile
160+
profile, err := bwReadProfile()
161+
if err != nil {
162+
return err
163+
}
164+
167165
// Load session or create a new one
168166
session, err := bwReadSession()
169167
if err != nil {
170168
return err
171169
}
172170

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")
171+
// If no password is set, then read it
172+
password := bwPassword
173+
if password == "" {
174+
stdin := int(os.Stdin.Fd())
175+
if !term.IsTerminal(stdin) {
176+
return ErrBadParameter.With("No password set and not running in terminal")
177+
}
178+
fmt.Fprintf(os.Stdout, "Enter password: ")
179+
if value, err := term.ReadPassword(stdin); err != nil {
180+
return err
181+
} else {
182+
password = string(value)
183+
}
184+
fmt.Fprintf(os.Stdout, "\n")
185+
}
186+
187+
// Make the decryption key
188+
if err := session.CacheKey(profile.Key, profile.Email, password); err != nil {
189+
return err
176190
}
177191

178192
// Read the folders
@@ -186,7 +200,7 @@ func bwFolders(w *tablewriter.TableWriter) error {
186200
if decrypted, err := folder.Decrypt(session); err != nil {
187201
return err
188202
} else {
189-
folders[i] = decrypted
203+
folders[i] = decrypted.(*schema.Folder)
190204
}
191205
}
192206

@@ -200,12 +214,35 @@ func bwFolders(w *tablewriter.TableWriter) error {
200214
///////////////////////////////////////////////////////////////////////////////
201215
// OTHER
202216

203-
func bwReadSession() (*bitwarden.Session, error) {
204-
result := new(bitwarden.Session)
217+
func bwReadProfile() (*schema.Profile, error) {
218+
result := schema.NewProfile()
219+
filename := filepath.Join(bwConfigDir, "profile.json")
220+
if _, err := os.Stat(filename); os.IsNotExist(err) {
221+
// Return an error
222+
return nil, ErrNotFound.With("Profile not found")
223+
} else if err != nil {
224+
return nil, err
225+
}
226+
227+
// Open the file
228+
file, err := os.Open(filename)
229+
if err != nil {
230+
return nil, err
231+
}
232+
defer file.Close()
233+
234+
// Read and return the session
235+
return result, result.Read(file)
236+
}
237+
238+
func bwReadSession() (*schema.Session, error) {
239+
result := schema.NewSession()
205240
filename := filepath.Join(bwConfigDir, "session.json")
206241
if _, err := os.Stat(filename); os.IsNotExist(err) {
207242
// Return a new, empty session
208243
return result, nil
244+
} else if err != nil {
245+
return nil, err
209246
}
210247

211248
// Open the file
@@ -219,7 +256,7 @@ func bwReadSession() (*bitwarden.Session, error) {
219256
return result, result.Read(file)
220257
}
221258

222-
func bwWriteSession(session *bitwarden.Session) error {
259+
func bwWriteSession(session *schema.Session) error {
223260
return bwWrite("session.json", session)
224261
}
225262

pkg/bitwarden/client.go

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ type Client struct {
2121
*client.Client
2222
}
2323

24-
type reqToken struct {
24+
type Login struct {
2525
GrantType string `json:"grant_type"`
2626
Scope string `json:"scope"`
2727
ClientId string `json:"client_id"`
2828
ClientSecret string `json:"client_secret"`
29+
}
2930

30-
// Device
31+
type reqToken struct {
32+
Login
3133
*schema.Device
3234

3335
// Two-factor authentication
@@ -77,21 +79,13 @@ func New(opts ...client.ClientOpt) (*Client, error) {
7779
return &Client{client}, nil
7880
}
7981

80-
// Create a new empty session
81-
func NewSession() *schema.Session {
82-
session := new(schema.Session)
83-
session.grantType = defaultGrantType
84-
session.scope = defaultScope
85-
return session
86-
}
87-
8882
///////////////////////////////////////////////////////////////////////////////
8983
// PUBLIC METHODS
9084

9185
// Login updates a session with a token. To create a new, empty session
9286
// then pass an empty session to this method. Use OptCredentials option
9387
// to pass a client_id and client_secret to the session.
94-
func (c *Client) Login(session *schema.Session, opts ...SessionOpt) error {
88+
func (c *Client) Login(session *schema.Session, opts ...LoginOpt) error {
9589
var request reqToken
9690
var response respToken
9791

@@ -100,18 +94,21 @@ func (c *Client) Login(session *schema.Session, opts ...SessionOpt) error {
10094
return ErrBadParameter.With("session")
10195
}
10296

97+
// Set defaults
98+
request.GrantType = defaultGrantType
99+
request.Scope = defaultScope
100+
103101
// Apply options
104102
for _, opt := range opts {
105-
if err := opt(session); err != nil {
103+
if err := opt(session, &request); err != nil {
106104
return err
107105
}
108106
}
109107

110-
// Check
111-
if session.clientId == "" || session.clientSecret == "" {
108+
// Check request parameters
109+
if request.ClientId == "" || request.ClientSecret == "" {
112110
return ErrBadParameter.With("missing credentials")
113-
}
114-
if session.Device == nil {
111+
} else if session.Device == nil {
115112
return ErrBadParameter.With("missing device")
116113
} else {
117114
// Populate missing device fields
@@ -132,14 +129,8 @@ func (c *Client) Login(session *schema.Session, opts ...SessionOpt) error {
132129
return nil
133130
}
134131

135-
// Set up the request
136-
request.GrantType = session.grantType
137-
request.Scope = session.scope
138-
request.ClientId = session.clientId
139-
request.ClientSecret = session.clientSecret
140-
request.Device = session.Device
141-
142132
// Request -> Response
133+
request.Device = session.Device
143134
if payload, err := client.NewFormRequest(request, client.ContentTypeJson); err != nil {
144135
return err
145136
} else if err := c.Do(payload, &response, client.OptReqEndpoint(identityUrl), client.OptPath("connect/token")); err != nil {

pkg/bitwarden/opt.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import (
1111
///////////////////////////////////////////////////////////////////////////////
1212
// TYPES
1313

14-
type SessionOpt func(*schema.Session) error
14+
type LoginOpt func(*schema.Session, *reqToken) error
1515

1616
///////////////////////////////////////////////////////////////////////////////
1717
// SESSION OPTIONS
1818

1919
// Set the device, populating missing fields
20-
func OptDevice(device schema.Device) SessionOpt {
21-
return func(s *schema.Session) error {
20+
func OptDevice(device schema.Device) LoginOpt {
21+
return func(s *schema.Session, r *reqToken) error {
2222
if device.Name == "" {
2323
return ErrBadParameter.With("OptDevice")
2424
} else {
@@ -36,19 +36,20 @@ func OptDevice(device schema.Device) SessionOpt {
3636
}
3737

3838
// Set the client_id and client_secret
39-
func OptCredentials(clientId, secret string) SessionOpt {
40-
return func(s *schema.Session) error {
39+
func OptCredentials(clientId, secret string) LoginOpt {
40+
return func(s *schema.Session, r *reqToken) error {
4141
if clientId == "" || secret == "" {
4242
return ErrBadParameter.With("OptCredentials")
4343
}
44-
s.SetCredentials(clientId, secret)
44+
r.ClientId = clientId
45+
r.ClientSecret = secret
4546
return nil
4647
}
4748
}
4849

4950
// Force login by clearing the token
50-
func OptForce() SessionOpt {
51-
return func(s *schema.Session) error {
51+
func OptForce() LoginOpt {
52+
return func(s *schema.Session, r *reqToken) error {
5253
s.Token = nil
5354
return nil
5455
}

pkg/bitwarden/schema/folder.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package schema
22

33
import (
44
"encoding/json"
5-
"fmt"
65
"io"
76
"time"
87
)
@@ -14,7 +13,7 @@ type Folders []*Folder
1413

1514
type Folder struct {
1615
Id string `json:"id"`
17-
Name string `json:"name"`
16+
Name string `json:"name"` // Encrypted
1817
RevisionDate time.Time `json:"revisionDate"`
1918
Object string `json:"object"`
2019
}
@@ -34,8 +33,13 @@ func (f *Folders) Write(w io.Writer) error {
3433

3534
// Decrypt a folder
3635
func (f Folder) Decrypt(s *Session) (Crypter, error) {
37-
fmt.Println("TODO: Decrypt")
38-
return f, nil
36+
result := &f
37+
if value, err := s.DecryptStr(result.Name); err != nil {
38+
return nil, err
39+
} else {
40+
result.Name = value
41+
}
42+
return &f, nil
3943
}
4044

4145
///////////////////////////////////////////////////////////////////////////////

pkg/bitwarden/schema/profile.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,25 @@ type Profile struct {
1717
Premium bool `json:"premium"`
1818
PremiumFromOrganization bool `json:"premiumFromOrganization"`
1919
MasterPasswordHash string `json:"masterPasswordHash"`
20-
MasterPasswordHint *string `json:"masterPasswordHint"`
20+
MasterPasswordHint *string `json:"masterPasswordHint,omitempty"`
2121
Culture string `json:"culture"`
2222
TwoFactorEnabled bool `json:"twoFactorEnabled"`
23-
SecurityStamp *string `json:"securityStamp"`
23+
SecurityStamp *string `json:"securityStamp,omitempty"`
2424
ForcePasswordReset bool `json:"forcePasswordReset"`
2525
UsesKeyConnector bool `json:"usesKeyConnector"`
26-
Organizations []*Organization `json:"organizations"`
26+
Organizations []*Organization `json:"organizations,omitempty"`
2727
Object string `json:"object"`
2828
}
2929

30+
///////////////////////////////////////////////////////////////////////////////
31+
// LIFECYCLE
32+
33+
func NewProfile() *Profile {
34+
return &Profile{
35+
Object: "profile",
36+
}
37+
}
38+
3039
///////////////////////////////////////////////////////////////////////////////
3140
// STRINGIFY
3241

0 commit comments

Comments
 (0)