package buildblob import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/json" "errors" "fmt" "os" "golang.org/x/crypto/argon2" ) const ( keyLen = 32 defaultTime = 3 defaultMem = 64 * 1024 defaultCPU = 4 ) type Manifest struct { OAuthClientID string `json:"oauthClientID"` OAuthClientSecret string `json:"oauthClientSecret"` Tags []string `json:"tags"` Users []ManifestUser `json:"users"` } type ManifestUser struct { Username string `json:"username"` Password string `json:"password"` } type Blob struct { Version int `json:"version"` Users []BlobUser `json:"users"` KDF KDFParams `json:"kdf"` } type BlobUser struct { Username string `json:"username"` Salt []byte `json:"salt"` Nonce []byte `json:"nonce"` Ciphertext []byte `json:"ciphertext"` } type KDFParams struct { Time uint32 `json:"time"` Memory uint32 `json:"memory"` Threads uint8 `json:"threads"` KeyLen uint32 `json:"keyLen"` } type payload struct { OAuthClientID string `json:"oauthClientID"` OAuthClientSecret string `json:"oauthClientSecret"` Tags []string `json:"tags"` } func BuildFromFile(path string) ([]byte, error) { raw, err := os.ReadFile(path) if err != nil { return nil, err } var manifest Manifest if err := json.Unmarshal(raw, &manifest); err != nil { return nil, err } return Build(manifest) } func Build(manifest Manifest) ([]byte, error) { if manifest.OAuthClientID == "" || manifest.OAuthClientSecret == "" { return nil, errors.New("manifest must include oauthClientID and oauthClientSecret") } if len(manifest.Users) == 0 { return nil, errors.New("manifest must include at least one user") } pt, err := json.Marshal(payload{ OAuthClientID: manifest.OAuthClientID, OAuthClientSecret: manifest.OAuthClientSecret, Tags: manifest.Tags, }) if err != nil { return nil, err } params := KDFParams{Time: defaultTime, Memory: defaultMem, Threads: defaultCPU, KeyLen: keyLen} blob := Blob{Version: 1, KDF: params} seen := map[string]struct{}{} for _, user := range manifest.Users { if user.Username == "" || user.Password == "" { return nil, errors.New("each user must include username and password") } if _, ok := seen[user.Username]; ok { return nil, fmt.Errorf("duplicate username %q", user.Username) } seen[user.Username] = struct{}{} salt := make([]byte, 16) if _, err := rand.Read(salt); err != nil { return nil, err } key := argon2.IDKey([]byte(user.Password), salt, params.Time, params.Memory, params.Threads, params.KeyLen) block, err := aes.NewCipher(key) zero(key) if err != nil { return nil, err } aead, err := cipher.NewGCM(block) if err != nil { return nil, err } nonce := make([]byte, aead.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, err } blob.Users = append(blob.Users, BlobUser{ Username: user.Username, Salt: salt, Nonce: nonce, Ciphertext: aead.Seal(nil, nonce, pt, nil), }) } return json.Marshal(blob) } func zero(buf []byte) { for i := range buf { buf[i] = 0 } }