package rands import ( "crypto/rand" "encoding/base64" "encoding/hex" "fmt" "math/big" "unicode" ) const ( upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" lowerChars = "abcdefghijklmnopqrstuvwxyz" numericChars = "0123456789" lowerNumericChars = lowerChars + numericChars upperNumericChars = upperChars + numericChars alphabeticChars = upperChars + lowerChars alphanumericChars = alphabeticChars + numericChars dnsLabelChars = lowerNumericChars + "-" ) var ( errNonASCIIAlphabet = fmt.Errorf( "%w: alphabet contains non-ASCII characters", errBase, ) errDNSLabelLength = fmt.Errorf( "%w: DNS labels must be between 1 and 63 characters in length", errBase, ) ) // Base64 generates a random base64 encoded string of n number of bytes. // // Length of the returned string is about one third greater than the value of n, // and it may contain characters A-Z, a-z, 0-9, "+", "/", and "=". func Base64(n int) (string, error) { b, err := Bytes(n) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(b), nil } // Base64URL generates a URL-safe un-padded random base64 encoded string of n // number of bytes. // // Length of the returned string is about one third greater than the value of n, // and it may contain characters A-Z, a-z, 0-9, "-", and "_". func Base64URL(n int) (string, error) { b, err := Bytes(n) if err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b), nil } // Hex generates a random hexadecimal encoded string of n number of bytes. // // Length of the returned string is twice the value of n, and it may contain // characters 0-9 and a-f. func Hex(n int) (string, error) { b, err := Bytes(n) if err != nil { return "", err } return hex.EncodeToString(b), nil } // Alphanumeric generates a random alphanumeric string of n length. // // The returned string may contain A-Z, a-z, and 0-9. func Alphanumeric(n int) (string, error) { return String(n, alphanumericChars) } // Alphabetic generates a random alphabetic string of n length. // // The returned string may contain A-Z, and a-z. func Alphabetic(n int) (string, error) { return String(n, alphabeticChars) } // Numeric generates a random numeric string of n length. // // The returned string may contain 0-9. func Numeric(n int) (string, error) { return String(n, numericChars) } // Upper generates a random uppercase alphabetic string of n length. // // The returned string may contain A-Z. func Upper(n int) (string, error) { return String(n, upperChars) } // UpperNumeric generates a random uppercase alphanumeric string of n length. // // The returned string may contain A-Z and 0-9. func UpperNumeric(n int) (string, error) { return String(n, upperNumericChars) } // Lower generates a random lowercase alphabetic string of n length. // // The returned string may contain a-z. func Lower(n int) (string, error) { return String(n, lowerChars) } // LowerNumeric generates a random lowercase alphanumeric string of n length. // // The returned string may contain A-Z and 0-9. func LowerNumeric(n int) (string, error) { return String(n, lowerNumericChars) } // String generates a random string of n length using the given ASCII alphabet. // // The specified alphabet determines what characters are used in the returned // random string. The alphabet can only contain ASCII characters, use // UnicodeString() if you need a alphabet with Unicode characters. func String(n int, alphabet string) (string, error) { if !isASCII(alphabet) { return "", errNonASCIIAlphabet } l := big.NewInt(int64(len(alphabet))) b := make([]byte, n) for i := 0; i < n; i++ { index, err := rand.Int(rand.Reader, l) if err != nil { return "", err } b[i] = alphabet[index.Int64()] } return string(b), nil } // UnicodeString generates a random string of n length using the given Unicode // alphabet. // // The specified alphabet determines what characters are used in the returned // random string. The length of the returned string will be n or greater // depending on the byte-length of characters which were randomly selected from // the alphabet. func UnicodeString(n int, alphabet []rune) (string, error) { l := big.NewInt(int64(len(alphabet))) b := make([]rune, n) for i := 0; i < n; i++ { index, err := rand.Int(rand.Reader, l) if err != nil { return "", err } b[i] = alphabet[index.Int64()] } return string(b), nil } // DNSLabel returns a random string of n length in a DNS label compliant format // as defined in RFC 1035, section 2.3.1, Preferred name syntax: // https://tools.ietf.org/html/rfc1035#section-2.3.1 // // It also adheres to RFC 5891, section 4.2.3.1, Hyphen Restrictions: // https://tools.ietf.org/html/rfc5891#section-4.2.3.1 // // In summary, the generated random string will: // // - be between 1 and 63 characters in length, other n values returns a error // - first character will be one of a-z // - last character will be one of a-z or 0-9 // - in-between first and last characters consist of a-z, 0-9, or "-" // - potentially contain two or more consecutive "-", except the 3rd and 4th // characters, as that would violate RFC 5891. func DNSLabel(n int) (string, error) { switch { case n < 1 || n > 63: return "", errDNSLabelLength case n == 1: return String(1, lowerChars) default: // First character of a DNS label allows only a-z characters. head, err := String(1, lowerChars) if err != nil { return "", err } // Last character of a DNS label allows only a-z and 0-9 characters. tail, err := String(1, lowerNumericChars) if err != nil { return "", err } if n < 3 { return head + tail, nil } // The middle of a DNS label allows only a-z, 0-9, and "-" characters. bodyLen := n - 2 body := make([]byte, bodyLen) var last byte var l *big.Int for i := 0; i < bodyLen; i++ { // Prevent two consecutive hyphens characters in positions 3 and 4, // in accordance RFC 5891, section 4.2.3.1, Hyphen Restrictions: // https://tools.ietf.org/html/rfc5891#section-4.2.3.1 if i == 2 && last == byte(45) { l = big.NewInt(int64(len(lowerNumericChars))) } else { l = big.NewInt(int64(len(dnsLabelChars))) } index, err := rand.Int(rand.Reader, l) if err != nil { return "", err } if i == 2 && last == byte(45) { last = lowerNumericChars[index.Int64()] } else { last = dnsLabelChars[index.Int64()] } body[i] = last } return head + string(body) + tail, nil } } func isASCII(s string) bool { for _, c := range s { if c > unicode.MaxASCII { return false } } return true }