Skip to content

Commit 53119d1

Browse files
authored
Support configurable TOTP in credential commands (#277)
## Summary - Add TOTP algorithm, digits, and period flags to credential create/update. - Use typed Go SDK v0.116.0 request fields; preserve omitted values on updates. - Document provisioning URI input and remove the six-digit assumption from code help. ## Checks - `go test ./...` passed locally.
1 parent a31208e commit 53119d1

4 files changed

Lines changed: 154 additions & 18 deletions

File tree

‎cmd/credentials.go‎

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,25 @@ type CredentialsGetInput struct {
4444
}
4545

4646
type CredentialsCreateInput struct {
47-
Name string
48-
Domain string
49-
Values map[string]string
50-
SSOProvider string
51-
TotpSecret string
52-
Output string
47+
Name string
48+
Domain string
49+
Values map[string]string
50+
SSOProvider string
51+
TotpSecret string
52+
TotpAlgorithm string
53+
TotpDigits *int
54+
TotpPeriod *int
55+
Output string
5356
}
5457

5558
type CredentialsUpdateInput struct {
5659
Identifier string
5760
Name string
5861
SSOProvider string
5962
TotpSecret string
63+
TotpAlgorithm string
64+
TotpDigits *int
65+
TotpPeriod *int
6066
Values map[string]string
6167
RemoveValueKeys []string
6268
Output string
@@ -176,6 +182,29 @@ func (c CredentialsCmd) Get(ctx context.Context, in CredentialsGetInput) error {
176182
return nil
177183
}
178184

185+
func validateTotpSettings(secret, algorithm string, digits, period *int) error {
186+
if algorithm == "" && digits == nil && period == nil {
187+
return nil
188+
}
189+
if secret == "" {
190+
return fmt.Errorf("TOTP settings require --totp-secret")
191+
}
192+
if algorithm != "" {
193+
switch strings.ToUpper(algorithm) {
194+
case "SHA1", "SHA256", "SHA512":
195+
default:
196+
return fmt.Errorf("--totp-algorithm must be SHA1, SHA256, or SHA512")
197+
}
198+
}
199+
if digits != nil && (*digits < 6 || *digits > 9) {
200+
return fmt.Errorf("--totp-digits must be between 6 and 9")
201+
}
202+
if period != nil && (*period < 15 || *period > 300) {
203+
return fmt.Errorf("--totp-period must be between 15 and 300")
204+
}
205+
return nil
206+
}
207+
179208
func (c CredentialsCmd) Create(ctx context.Context, in CredentialsCreateInput) error {
180209
if err := validateJSONOutput(in.Output); err != nil {
181210
return err
@@ -191,6 +220,9 @@ func (c CredentialsCmd) Create(ctx context.Context, in CredentialsCreateInput) e
191220
return fmt.Errorf("at least one --value is required")
192221
}
193222

223+
if err := validateTotpSettings(in.TotpSecret, in.TotpAlgorithm, in.TotpDigits, in.TotpPeriod); err != nil {
224+
return err
225+
}
194226
params := kernel.CredentialNewParams{
195227
CreateCredentialRequest: kernel.CreateCredentialRequestParam{
196228
Name: in.Name,
@@ -204,6 +236,15 @@ func (c CredentialsCmd) Create(ctx context.Context, in CredentialsCreateInput) e
204236
if in.TotpSecret != "" {
205237
params.CreateCredentialRequest.TotpSecret = kernel.Opt(in.TotpSecret)
206238
}
239+
if in.TotpAlgorithm != "" {
240+
params.CreateCredentialRequest.TotpAlgorithm = kernel.CreateCredentialRequestTotpAlgorithm(strings.ToUpper(in.TotpAlgorithm))
241+
}
242+
if in.TotpDigits != nil {
243+
params.CreateCredentialRequest.TotpDigits = kernel.Int(int64(*in.TotpDigits))
244+
}
245+
if in.TotpPeriod != nil {
246+
params.CreateCredentialRequest.TotpPeriod = kernel.Int(int64(*in.TotpPeriod))
247+
}
207248

208249
if in.Output != "json" {
209250
pterm.Info.Printf("Creating credential '%s'...\n", in.Name)
@@ -252,6 +293,9 @@ func (c CredentialsCmd) Update(ctx context.Context, in CredentialsUpdateInput) e
252293
if err := validateJSONOutput(in.Output); err != nil {
253294
return err
254295
}
296+
if err := validateTotpSettings(in.TotpSecret, in.TotpAlgorithm, in.TotpDigits, in.TotpPeriod); err != nil {
297+
return err
298+
}
255299

256300
params := kernel.CredentialUpdateParams{
257301
UpdateCredentialRequest: kernel.UpdateCredentialRequestParam{},
@@ -265,6 +309,15 @@ func (c CredentialsCmd) Update(ctx context.Context, in CredentialsUpdateInput) e
265309
if in.TotpSecret != "" {
266310
params.UpdateCredentialRequest.TotpSecret = kernel.Opt(in.TotpSecret)
267311
}
312+
if in.TotpAlgorithm != "" {
313+
params.UpdateCredentialRequest.TotpAlgorithm = kernel.UpdateCredentialRequestTotpAlgorithm(strings.ToUpper(in.TotpAlgorithm))
314+
}
315+
if in.TotpDigits != nil {
316+
params.UpdateCredentialRequest.TotpDigits = kernel.Int(int64(*in.TotpDigits))
317+
}
318+
if in.TotpPeriod != nil {
319+
params.UpdateCredentialRequest.TotpPeriod = kernel.Int(int64(*in.TotpPeriod))
320+
}
268321
if len(in.Values) > 0 {
269322
params.UpdateCredentialRequest.Values = in.Values
270323
}
@@ -398,7 +451,7 @@ var credentialsDeleteCmd = &cobra.Command{
398451
var credentialsTotpCodeCmd = &cobra.Command{
399452
Use: "totp-code <id-or-name>",
400453
Short: "Get the current TOTP code for a credential",
401-
Long: `Returns the current 6-digit TOTP code for a credential with a configured totp_secret.`,
454+
Long: `Returns the current TOTP code for a credential with a configured totp_secret.`,
402455
Args: cobra.ExactArgs(1),
403456
RunE: runCredentialsTotpCode,
404457
}
@@ -427,15 +480,21 @@ func init() {
427480
credentialsCreateCmd.Flags().String("domain", "", "Target domain this credential is for (required)")
428481
credentialsCreateCmd.Flags().StringArray("value", []string{}, "Field name=value pair (repeatable, e.g., --value username=myuser --value password=mypass)")
429482
credentialsCreateCmd.Flags().String("sso-provider", "", "SSO provider (e.g., google, github, microsoft)")
430-
credentialsCreateCmd.Flags().String("totp-secret", "", "Base32-encoded TOTP secret for 2FA")
483+
credentialsCreateCmd.Flags().String("totp-secret", "", "Base32 secret (16-128 characters) or otpauth:// URI for 2FA")
484+
credentialsCreateCmd.Flags().String("totp-algorithm", "", "TOTP algorithm: SHA1, SHA256, or SHA512 (default SHA1)")
485+
credentialsCreateCmd.Flags().Int("totp-digits", 6, "TOTP code digits: 6-9 (default 6)")
486+
credentialsCreateCmd.Flags().Int("totp-period", 30, "TOTP period in seconds: 15-300 (default 30)")
431487
_ = credentialsCreateCmd.MarkFlagRequired("name")
432488
_ = credentialsCreateCmd.MarkFlagRequired("domain")
433489

434490
// Update flags
435491
addJSONOutputFlag(credentialsUpdateCmd)
436492
credentialsUpdateCmd.Flags().String("name", "", "New name for the credential")
437493
credentialsUpdateCmd.Flags().String("sso-provider", "", "SSO provider (set to empty string to remove)")
438-
credentialsUpdateCmd.Flags().String("totp-secret", "", "Base32-encoded TOTP secret (set to empty string to remove)")
494+
credentialsUpdateCmd.Flags().String("totp-secret", "", "Base32 secret (16-128 characters) or otpauth:// URI")
495+
credentialsUpdateCmd.Flags().String("totp-algorithm", "", "TOTP algorithm: SHA1, SHA256, or SHA512")
496+
credentialsUpdateCmd.Flags().Int("totp-digits", 6, "TOTP code digits: 6-9")
497+
credentialsUpdateCmd.Flags().Int("totp-period", 30, "TOTP period in seconds: 15-300")
439498
credentialsUpdateCmd.Flags().StringArray("value", []string{}, "Field name=value pair to update (repeatable)")
440499
credentialsUpdateCmd.Flags().StringArray("remove-value-key", []string{}, "Field name to remove from the credential's stored values (repeatable). Removals are applied before --value is merged, so a key given to both keeps its new value")
441500

@@ -477,6 +536,13 @@ func runCredentialsGet(cmd *cobra.Command, args []string) error {
477536
})
478537
}
479538

539+
func optionalIntFlag(cmd *cobra.Command, name string, value int) *int {
540+
if cmd.Flags().Changed(name) {
541+
return &value
542+
}
543+
return nil
544+
}
545+
480546
func runCredentialsCreate(cmd *cobra.Command, args []string) error {
481547
client := getKernelClient(cmd)
482548
output, _ := cmd.Flags().GetString("output")
@@ -485,6 +551,9 @@ func runCredentialsCreate(cmd *cobra.Command, args []string) error {
485551
valuePairs, _ := cmd.Flags().GetStringArray("value")
486552
ssoProvider, _ := cmd.Flags().GetString("sso-provider")
487553
totpSecret, _ := cmd.Flags().GetString("totp-secret")
554+
algorithm, _ := cmd.Flags().GetString("totp-algorithm")
555+
digits, _ := cmd.Flags().GetInt("totp-digits")
556+
period, _ := cmd.Flags().GetInt("totp-period")
488557

489558
// Parse value pairs into map
490559
values := make(map[string]string)
@@ -499,12 +568,15 @@ func runCredentialsCreate(cmd *cobra.Command, args []string) error {
499568
svc := client.Credentials
500569
c := CredentialsCmd{credentials: &svc}
501570
return c.Create(cmd.Context(), CredentialsCreateInput{
502-
Name: name,
503-
Domain: domain,
504-
Values: values,
505-
SSOProvider: ssoProvider,
506-
TotpSecret: totpSecret,
507-
Output: output,
571+
Name: name,
572+
Domain: domain,
573+
Values: values,
574+
SSOProvider: ssoProvider,
575+
TotpSecret: totpSecret,
576+
TotpAlgorithm: algorithm,
577+
TotpDigits: optionalIntFlag(cmd, "totp-digits", digits),
578+
TotpPeriod: optionalIntFlag(cmd, "totp-period", period),
579+
Output: output,
508580
})
509581
}
510582

@@ -514,6 +586,9 @@ func runCredentialsUpdate(cmd *cobra.Command, args []string) error {
514586
name, _ := cmd.Flags().GetString("name")
515587
ssoProvider, _ := cmd.Flags().GetString("sso-provider")
516588
totpSecret, _ := cmd.Flags().GetString("totp-secret")
589+
algorithm, _ := cmd.Flags().GetString("totp-algorithm")
590+
digits, _ := cmd.Flags().GetInt("totp-digits")
591+
period, _ := cmd.Flags().GetInt("totp-period")
517592
valuePairs, _ := cmd.Flags().GetStringArray("value")
518593
removeValueKeys, _ := cmd.Flags().GetStringArray("remove-value-key")
519594

@@ -534,6 +609,9 @@ func runCredentialsUpdate(cmd *cobra.Command, args []string) error {
534609
Name: name,
535610
SSOProvider: ssoProvider,
536611
TotpSecret: totpSecret,
612+
TotpAlgorithm: algorithm,
613+
TotpDigits: optionalIntFlag(cmd, "totp-digits", digits),
614+
TotpPeriod: optionalIntFlag(cmd, "totp-period", period),
537615
Values: values,
538616
RemoveValueKeys: removeValueKeys,
539617
Output: output,

‎cmd/credentials_totp_test.go‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/kernel/kernel-go-sdk"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestTypedTotpFields(t *testing.T) {
12+
digits, period := 8, 60
13+
require.NoError(t, validateTotpSettings("A234567A234567A2", "sha512", &digits, &period))
14+
params := kernel.CreateCredentialRequestParam{
15+
Domain: "example.com",
16+
Name: "test",
17+
Values: map[string]string{"username": "test"},
18+
TotpSecret: kernel.String("A234567A234567A2"),
19+
TotpAlgorithm: kernel.CreateCredentialRequestTotpAlgorithmSha512,
20+
TotpDigits: kernel.Int(8),
21+
TotpPeriod: kernel.Int(60),
22+
}
23+
encoded, err := json.Marshal(params)
24+
require.NoError(t, err)
25+
var body map[string]any
26+
require.NoError(t, json.Unmarshal(encoded, &body))
27+
require.Equal(t, "SHA512", body["totp_algorithm"])
28+
require.Equal(t, float64(8), body["totp_digits"])
29+
require.Equal(t, float64(60), body["totp_period"])
30+
31+
update := kernel.UpdateCredentialRequestParam{
32+
TotpSecret: kernel.String("A234567A234567A2"),
33+
TotpAlgorithm: kernel.UpdateCredentialRequestTotpAlgorithmSha512,
34+
TotpDigits: kernel.Int(8),
35+
TotpPeriod: kernel.Int(60),
36+
}
37+
encoded, err = json.Marshal(update)
38+
require.NoError(t, err)
39+
body = make(map[string]any)
40+
require.NoError(t, json.Unmarshal(encoded, &body))
41+
require.Equal(t, "SHA512", body["totp_algorithm"])
42+
require.Equal(t, float64(8), body["totp_digits"])
43+
require.Equal(t, float64(60), body["totp_period"])
44+
45+
for _, test := range []struct {
46+
secret string
47+
algorithm string
48+
digits *int
49+
period *int
50+
}{
51+
{algorithm: "SHA512"},
52+
{secret: "A234567A234567A2", algorithm: "SHA224"},
53+
{secret: "A234567A234567A2", digits: new(int)},
54+
{secret: "A234567A234567A2", period: new(int)},
55+
} {
56+
require.Error(t, validateTotpSettings(test.secret, test.algorithm, test.digits, test.period))
57+
}
58+
}

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
1010
github.com/golang-jwt/jwt/v5 v5.2.2
1111
github.com/joho/godotenv v1.5.1
12-
github.com/kernel/kernel-go-sdk v0.114.1-0.20260930182635-e746d9980b83
12+
github.com/kernel/kernel-go-sdk v0.116.0
1313
github.com/klauspost/compress v1.18.5
1414
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
1515
github.com/pterm/pterm v0.12.80

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
6666
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
6767
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
6868
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
69-
github.com/kernel/kernel-go-sdk v0.114.1-0.20260930182635-e746d9980b83 h1:S0qtghU55P043VAtKzRzoPM9Ix8OcsEbTQNGsnoWN3U=
70-
github.com/kernel/kernel-go-sdk v0.114.1-0.20260930182635-e746d9980b83/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
69+
github.com/kernel/kernel-go-sdk v0.116.0 h1:NwZwl40sJ11lI8aHYSmMa0b6eXIXoZQVuH6UfPUaZX0=
70+
github.com/kernel/kernel-go-sdk v0.116.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
7171
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
7272
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
7373
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=

0 commit comments

Comments
 (0)