1import logger from '../logger'
2import * as Types from '../constants/types/crypto'
3import * as Constants from '../constants/crypto'
4import * as Container from '../util/container'
5import * as TeamBuildingGen from '../actions/team-building-gen'
6import * as CryptoGen from '../actions/crypto-gen'
7import HiddenString from '../util/hidden-string'
8import {editTeambuildingDraft} from './team-building'
9import {teamBuilderReducerCreator} from '../team-building/reducer-helper'
10
11const initialState: Types.State = Constants.makeState()
12
13type Actions = CryptoGen.Actions | TeamBuildingGen.Actions
14
15const operationGuard = (operation: Types.Operations, action: CryptoGen.Actions) => {
16  if (operation) return false
17
18  logger.error(
19    `Crypto reducer: Action (${action.type}) did not contain operation ( "encrypt", "decrypt", "verify", "sign" )`
20  )
21  return true
22}
23
24const resetOutput = (op: Types.CommonState) => {
25  op.output = new HiddenString('')
26  op.outputStatus = undefined
27  op.outputType = undefined
28  op.outputSenderUsername = undefined
29  op.outputSenderFullname = undefined
30  op.outputValid = false
31  op.errorMessage = new HiddenString('')
32  op.warningMessage = new HiddenString('')
33}
34
35export default Container.makeReducer<Actions, Types.State>(initialState, {
36  [CryptoGen.resetStore]: () => {
37    return initialState
38  },
39  [CryptoGen.resetOperation]: (draftState, action) => {
40    const {operation} = action.payload
41
42    if (operationGuard(operation, action)) return
43
44    switch (operation) {
45      case Constants.Operations.Encrypt: {
46        draftState.encrypt = initialState.encrypt
47        break
48      }
49      case Constants.Operations.Decrypt: {
50        draftState.decrypt = initialState.decrypt
51        break
52      }
53      case Constants.Operations.Sign: {
54        draftState.sign = initialState.sign
55        break
56      }
57      case Constants.Operations.Verify: {
58        draftState.verify = initialState.verify
59        break
60      }
61    }
62  },
63  [CryptoGen.clearInput]: (draftState, action) => {
64    const {operation} = action.payload
65
66    if (operationGuard(operation, action)) return
67
68    const op = draftState[operation]
69    op.bytesComplete = 0
70    op.bytesTotal = 0
71    op.inputType = 'text'
72    op.input = new HiddenString('')
73    op.output = new HiddenString('')
74    op.outputStatus = undefined
75    op.outputType = undefined
76    op.outputSenderUsername = undefined
77    op.outputSenderFullname = undefined
78    op.errorMessage = new HiddenString('')
79    op.warningMessage = new HiddenString('')
80    op.outputValid = true
81  },
82  [CryptoGen.clearRecipients]: (draftState, action) => {
83    const {operation} = action.payload
84
85    if (operationGuard(operation, action)) return
86
87    if (operation === Constants.Operations.Encrypt) {
88      const encrypt = draftState.encrypt
89      encrypt.bytesComplete = 0
90      encrypt.bytesTotal = 0
91      encrypt.recipients = initialState.encrypt.recipients
92      // Reset options since they depend on the recipients
93      encrypt.options = initialState.encrypt.options
94      encrypt.meta = initialState.encrypt.meta
95      encrypt.output = new HiddenString('')
96      encrypt.outputStatus = undefined
97      encrypt.outputType = undefined
98      encrypt.outputSenderUsername = undefined
99      encrypt.outputSenderFullname = undefined
100      encrypt.outputValid = false
101      encrypt.errorMessage = new HiddenString('')
102      encrypt.warningMessage = new HiddenString('')
103    }
104  },
105  [CryptoGen.setRecipients]: (draftState, action) => {
106    const {operation, recipients, hasSBS} = action.payload
107
108    if (operationGuard(operation, action)) return
109
110    if (operation !== Constants.Operations.Encrypt) return
111
112    const op = draftState.encrypt
113    const {inputType} = op
114
115    // Reset output when file input changes
116    // Prompt for destination dir
117    if (inputType === 'file') {
118      resetOutput(op)
119    }
120
121    // Output no longer valid since recipients have changed
122    op.outputValid = false
123
124    if (!op.recipients.length && recipients.length) {
125      op.meta.hasRecipients = true
126      op.meta.hasSBS = hasSBS
127    }
128    // Force signing when user is SBS
129    if (hasSBS) {
130      op.options.sign = true
131    }
132
133    if (recipients) {
134      op.recipients = recipients
135    }
136  },
137  [CryptoGen.setEncryptOptions]: (draftState, action) => {
138    const {options: newOptions, hideIncludeSelf} = action.payload
139    const {encrypt} = draftState
140    const {inputType} = encrypt
141    const oldOptions = encrypt.options
142    encrypt.options = {
143      ...oldOptions,
144      ...newOptions,
145    }
146
147    // Reset output when file input changes
148    // Prompt for destination dir
149    if (inputType === 'file') {
150      resetOutput(encrypt)
151    }
152
153    // Output no longer valid since options have changed
154    encrypt.outputValid = false
155
156    // User set themselves as a recipient so don't show the 'includeSelf' option for encrypt (since they're encrypting to themselves)
157    if (hideIncludeSelf) {
158      encrypt.meta.hideIncludeSelf = hideIncludeSelf
159      encrypt.options.includeSelf = false
160    }
161  },
162  [CryptoGen.setInput]: (draftState, action) => {
163    const {operation, type, value} = action.payload
164    if (operationGuard(operation, action)) return
165
166    const op = draftState[operation]
167    const oldInput = op.input
168    // Reset input to 'text' when no value given (cleared input or removed file upload)
169    const inputType = value.stringValue() ? type : 'text'
170    const outputValid = oldInput.stringValue() === value.stringValue()
171
172    op.inputType = inputType
173    op.input = value
174    op.outputValid = outputValid
175    op.errorMessage = new HiddenString('')
176    op.warningMessage = new HiddenString('')
177
178    // Reset output when file input changes
179    // Prompt for destination dir
180    if (inputType === 'file') {
181      resetOutput(op)
182    }
183  },
184  [CryptoGen.runFileOperation]: (draftState, action) => {
185    const {operation} = action.payload
186    if (operationGuard(operation, action)) return
187
188    const op = draftState[operation]
189    op.outputValid = false
190    op.errorMessage = new HiddenString('')
191    op.warningMessage = new HiddenString('')
192  },
193  [CryptoGen.saltpackDone]: (draftState, action) => {
194    const {operation} = action.payload
195    const op = draftState[operation]
196    // For any file operation that completes, invalidate the output since multiple decrypt/verify operations will produce filenames with unqiue
197    // counters on the end (as to not overwrite any existing files in the user's FS).
198    // E.g. `${plaintextFilename} (n).ext`
199    op.outputValid = false
200    op.bytesComplete = 0
201    op.bytesTotal = 0
202    op.inProgress = false
203    op.outputStatus = 'pending'
204  },
205  [CryptoGen.onSaltpackOpenFile]: (draftState, action) => {
206    const {operation, path} = action.payload
207    const op = draftState[operation]
208    const {inProgress} = op
209
210    // Bail on setting operation input if another file RPC is in progress
211    if (inProgress) return
212    if (!path.stringValue()) return
213
214    resetOutput(op)
215    op.input = path
216    op.inputType = 'file'
217    op.errorMessage = new HiddenString('')
218    op.warningMessage = new HiddenString('')
219  },
220  [CryptoGen.onOperationSuccess]: (draftState, action) => {
221    const {
222      input,
223      operation,
224      output,
225      outputSigned,
226      outputSenderFullname,
227      outputSenderUsername,
228      outputType,
229      warning,
230      warningMessage,
231    } = action.payload
232    if (operationGuard(operation, action)) return
233
234    let inputAction:
235      | CryptoGen.SaltpackDecryptPayload
236      | CryptoGen.SaltpackEncryptPayload
237      | CryptoGen.SaltpackSignPayload
238      | CryptoGen.SaltpackVerifyPayload
239      | undefined
240
241    switch (input?.type) {
242      // fallthrough
243      case CryptoGen.saltpackDecrypt:
244      case CryptoGen.saltpackEncrypt:
245      case CryptoGen.saltpackSign:
246      case CryptoGen.saltpackVerify:
247        inputAction = input
248        break
249      default:
250        inputAction = undefined
251    }
252
253    let outputValid = false
254
255    const op = draftState[operation]
256
257    if (inputAction) {
258      outputValid = inputAction.payload.input.stringValue() === op.input.stringValue()
259
260      // If the store's input matches its output, then we don't need to update with the value of the returning RPC.
261      if (op.outputValid) {
262        return
263      }
264
265      // Otherwise show the output but don't let them interact with it because the output is stale (newer RPC coming back)
266      op.outputValid = outputValid
267    }
268
269    // Reset errors and warnings
270    op.errorMessage = new HiddenString('')
271    op.warningMessage = new HiddenString('')
272
273    // Warning was set alongside successful output
274    if (warning && warningMessage) {
275      op.warningMessage = warningMessage
276    }
277
278    op.output = output
279    op.outputStatus = 'success'
280    op.outputType = outputType
281    op.outputSigned = outputSigned
282    op.outputSenderUsername = outputSenderUsername
283    op.outputSenderFullname = outputSenderFullname
284  },
285  [CryptoGen.onOperationError]: (draftState, action) => {
286    const {operation, errorMessage} = action.payload
287    if (operationGuard(operation, action)) return
288
289    const op = draftState[operation]
290    // Clear output
291    op.output = new HiddenString('')
292    op.outputType = undefined
293
294    // Set error
295    op.outputStatus = 'error'
296    op.errorMessage = errorMessage
297  },
298  [CryptoGen.saltpackStart]: (draftState, action) => {
299    const {operation} = action.payload
300    if (operationGuard(operation, action)) return
301
302    // Gets the progress bar on screen sooner. This matters most when encrypting/signing a directory (since progress is slow)
303    const op = draftState[operation]
304    op.inProgress = true
305  },
306  [CryptoGen.saltpackProgress]: (draftState, action) => {
307    const {bytesComplete, bytesTotal, operation} = action.payload
308    if (operationGuard(operation, action)) return
309
310    const op = draftState[operation]
311    // The final progress notification might come after saltpackDone.
312    // Reset progress when finsihed
313    if (bytesComplete === bytesTotal) {
314      op.bytesComplete = 0
315      op.bytesTotal = 0
316      op.inProgress = false
317      op.outputStatus = 'pending'
318      return
319    }
320    op.bytesComplete = bytesComplete
321    op.bytesTotal = bytesTotal
322    op.inProgress = true
323  },
324
325  // Encrypt: Handle team building when selecting keybase users
326  ...teamBuilderReducerCreator<Types.State>(
327    (draftState: Container.Draft<Types.State>, action: TeamBuildingGen.Actions) => {
328      const val = editTeambuildingDraft('crypto', draftState.teamBuilding, action)
329      if (val !== undefined) {
330        draftState.teamBuilding = val
331      }
332    }
333  ),
334})
335