0

I have a type called Password that is simply a string. I'd like to implement the Stringer interface by providing a String() method that redacts the value.

// Password a secret string that should not be leaked accidentally
type Password string

func (p Password) String() string {
    return "*********" // redact the password
}

This works as I would expect, if I attempt to print a Password.

p := Password("password not leaked here!")
fmt.Printf("password = %v \n", p) 
// got...  password = ********* 

But if the Password is a field within another struct, my String() method does not get called.

// User has a name and password
type User struct {
    name     string
    password Password
}

user := User{"Fran", Password("password was leaked!")}
fmt.Printf("user = %+v \n", user) 
// got...      user = {name:Fran password:password was leaked!}
// expected... user = {name:Fran password:*********}

Is there a way to make this call my String() method? It appears that the code actually calls refect.Value.String() instead.

https://play.golang.org/p/voBrSiOy-ol

package main

import (
    "fmt"
)

// Password a secret string that should not be leaked accidentally
type Password string

func (p Password) String() string {
    return "*********" // redact the password
}

// User has a name and password
type User struct {
    name     string
    password Password
}

func main() {
    s := Password("password not leaked here!")
    fmt.Printf("password = %v \n", s) // password = ********* 

    user := User{"Fran", Password("password was leaked!")}
    fmt.Printf("user = %+v \n", user) // user = {name:Fran password:password was leaked!}
}
Nick Allen
  • 1,443
  • 1
  • 11
  • 29
  • 2
    You cannot really protect secrets very well this way. Better to encrypt it at rest. You want to prevent casual dumps in log files and such so don't go crazy. A programmer with debugger tools would defeat anything you used if they focused on it. – Zan Lynx Jan 30 '21 at 00:21
  • 1
    "Why doesn't ..." is answerd most of the time by careful reading of the documentation. – Volker Jan 30 '21 at 09:10

2 Answers2

7

This is from the package fmt documentation:

When printing a struct, fmt cannot and therefore does not invoke formatting methods such as Error or String on unexported fields.

The field password is not exported, so it is not inspected to figure out it implements Stringer. Export it, and it will work:

type User struct {
    name     string
    Password Password
}
Burak Serdar
  • 46,455
  • 3
  • 40
  • 59
0

If you want a "failsafe" to protect yourself against accidental default formatting, you can hide your password values behind pointers by boxing a *string in your Password type.

https://play.golang.org/p/vE-cEXHp2ii

package main

import (
    "fmt"
)

// Password a secret string that should not be leaked accidentally
type Password struct{
 *string
}
func (p Password) String() string {
    return "*********" // redact the password
}

func NewPassword(pword string) Password {
    s := new(string)
    *s = pword
    return Password{s}
}

// User has a name and password
type User struct {
    name     string
    password Password
}

func main() {
    user := User{"Fran", NewPassword("this password is safe from default formatting")}
    fmt.Printf("user = %+v \n", user)
}

Output:

user = {name:Fran password:{string:0xc00008a040}} 
Hymns For Disco
  • 7,530
  • 2
  • 17
  • 33