1// Copyright 2015 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package libkb
5
6import (
7	"encoding/base64"
8	"encoding/hex"
9	"errors"
10	"fmt"
11	"time"
12
13	keybase1 "github.com/keybase/client/go/protocol/keybase1"
14)
15
16const LoginSessionMemoryTimeout time.Duration = time.Minute * 5
17
18var ErrLoginSessionNotLoaded = errors.New("LoginSession not loaded")
19var ErrLoginSessionCleared = errors.New("LoginSession already cleared")
20
21type LoginSession struct {
22	sessionFor      string // set by constructor
23	salt            []byte // retrieved from server, or set by WithSalt constructor
24	loginSessionB64 string
25	loginSession    []byte    // decoded from above parameter
26	loaded          bool      // load state
27	cleared         bool      // clear state
28	createTime      time.Time // load time
29	Contextified
30}
31
32func NewLoginSession(g *GlobalContext, emailOrUsername string) *LoginSession {
33	return &LoginSession{
34		sessionFor:   emailOrUsername,
35		Contextified: NewContextified(g),
36	}
37}
38
39// Upon signup, a login session is created with a generated salt.
40func NewLoginSessionWithSalt(g *GlobalContext, emailOrUsername string, salt []byte) *LoginSession {
41	ls := NewLoginSession(g, emailOrUsername)
42	ls.salt = salt
43	// XXX are these right?  is this just so the salt can be retrieved?
44	ls.loaded = true
45	ls.cleared = true
46	return ls
47}
48
49func (s *LoginSession) Status() *keybase1.SessionStatus {
50	return &keybase1.SessionStatus{
51		SessionFor: s.sessionFor,
52		Loaded:     s.loaded,
53		Cleared:    s.cleared,
54		Expired:    !s.NotExpired(),
55		SaltOnly:   s.loaded && s.loginSession == nil && s.salt != nil,
56	}
57}
58
59func (s *LoginSession) Session() ([]byte, error) {
60	if s == nil {
61		return nil, ErrLoginSessionNotLoaded
62	}
63	if !s.loaded {
64		return nil, ErrLoginSessionNotLoaded
65	}
66	if s.cleared {
67		return nil, ErrLoginSessionCleared
68	}
69	return s.loginSession, nil
70}
71
72func (s *LoginSession) SessionEncoded() (string, error) {
73	if s == nil {
74		return "", ErrLoginSessionNotLoaded
75	}
76	if !s.loaded {
77		return "", ErrLoginSessionNotLoaded
78	}
79	if s.cleared {
80		return "", ErrLoginSessionCleared
81	}
82	return s.loginSessionB64, nil
83}
84
85func (s *LoginSession) ExistsFor(emailOrUsername string) bool {
86	if s == nil {
87		return false
88	}
89	if s.sessionFor != emailOrUsername {
90		return false
91	}
92	if s.cleared {
93		return false
94	}
95	if s.loginSession == nil {
96		return false
97	}
98	return true
99}
100
101func (s *LoginSession) NotExpired() bool {
102	now := s.G().Clock().Now()
103
104	if now.Sub(s.createTime) < LoginSessionMemoryTimeout {
105		return true
106	}
107	s.G().Log.Debug("login_session expired")
108	return false
109}
110
111// Clear removes the loginSession value from s. It does not
112// clear the salt. Unclear how this is useful.
113func (s *LoginSession) Clear() error {
114	if s == nil {
115		return nil
116	}
117	if !s.loaded {
118		return ErrLoginSessionNotLoaded
119	}
120	s.loginSession = nil
121	s.loginSessionB64 = ""
122	s.cleared = true
123	return nil
124}
125
126func (s *LoginSession) Salt() ([]byte, error) {
127	if s == nil {
128		return nil, ErrLoginSessionNotLoaded
129	}
130	if !s.loaded {
131		return nil, ErrLoginSessionNotLoaded
132	}
133	return s.salt, nil
134}
135
136func (s *LoginSession) Dump() {
137	if s == nil {
138		fmt.Printf("LoginSession Dump: nil\n")
139		return
140	}
141	fmt.Printf("sessionFor: %q\n", s.sessionFor)
142	fmt.Printf("loaded: %v\n", s.loaded)
143	fmt.Printf("cleared: %v\n", s.cleared)
144	fmt.Printf("salt: %x\n", s.salt)
145	fmt.Printf("loginSessionB64: %s\n", s.loginSessionB64)
146	fmt.Printf("\n")
147}
148
149func (s *LoginSession) Load(m MetaContext) error {
150	if s == nil {
151		return fmt.Errorf("LoginSession is nil")
152	}
153	if s.loaded && !s.cleared {
154		return fmt.Errorf("LoginSession already loaded for %s", s.sessionFor)
155	}
156
157	res, err := m.G().API.Get(m, APIArg{
158		Endpoint:    "getsalt",
159		SessionType: APISessionTypeNONE,
160		Args: HTTPArgs{
161			"email_or_username": S{Val: s.sessionFor},
162			"pdpka_login":       B{Val: true},
163		},
164	})
165	if err != nil {
166		return err
167	}
168
169	shex, err := res.Body.AtKey("salt").GetString()
170	if err != nil {
171		return err
172	}
173
174	salt, err := hex.DecodeString(shex)
175	if err != nil {
176		return err
177	}
178
179	b64, err := res.Body.AtKey("login_session").GetString()
180	if err != nil {
181		return err
182	}
183
184	ls, err := base64.StdEncoding.DecodeString(b64)
185	if err != nil {
186		return err
187	}
188
189	s.salt = salt
190	s.loginSessionB64 = b64
191	s.loginSession = ls
192	s.loaded = true
193	s.cleared = false
194	s.createTime = s.G().Clock().Now()
195
196	return nil
197}
198
199func LookupSaltForUID(m MetaContext, uid keybase1.UID) (salt []byte, err error) {
200	defer m.Trace(fmt.Sprintf("GetSaltForUID(%s)", uid), &err)()
201	res, err := m.G().API.Get(m, APIArg{
202		Endpoint:    "getsalt",
203		SessionType: APISessionTypeNONE,
204		Args: HTTPArgs{
205			"uid": S{Val: uid.String()},
206		},
207	})
208	if err != nil {
209		return nil, err
210	}
211	var shex string
212	shex, err = res.Body.AtKey("salt").GetString()
213	if err != nil {
214		return nil, err
215	}
216	salt, err = hex.DecodeString(shex)
217	if err != nil {
218		return nil, err
219	}
220	return salt, err
221}
222