cipherstring.go

v1.0.0
Doc Versions Source
1
package crypto
2
3
import (
4
	"encoding/base64"
5
	"fmt"
6
	"strconv"
7
	"strings"
8
)
9
10
// EncType represents the encryption type used in a CipherString.
11
type EncType int
12
13
const (
14
	EncAesCbc256_B64                     EncType = 0 // Legacy, no HMAC
15
	EncAesCbc128_HmacSha256_B64          EncType = 1
16
	EncAesCbc256_HmacSha256_B64          EncType = 2 // Standard
17
	EncRsa2048_OaepSha256_B64            EncType = 3
18
	EncRsa2048_OaepSha1_B64              EncType = 4 // Org key sharing
19
	EncRsa2048_OaepSha256_HmacSha256_B64 EncType = 5
20
	EncRsa2048_OaepSha1_HmacSha256_B64   EncType = 6
21
)
22
23
// CipherString represents an encrypted value in Bitwarden's format.
24
type CipherString struct {
25
	Type EncType
26
	IV   []byte // nil for RSA types
27
	CT   []byte // ciphertext
28
	MAC  []byte // nil for type 0 and RSA types without HMAC
29
}
30
31
// ParseCipherString parses a string like "2.iv_b64|ct_b64|mac_b64".
32
func ParseCipherString(s string) (*CipherString, error) {
33
	if s == "" {
34
		return nil, fmt.Errorf("empty cipher string")
35
	}
36
37
	parts := strings.SplitN(s, ".", 2)
38
	if len(parts) != 2 {
39
		return nil, fmt.Errorf("invalid cipher string format: missing type prefix")
40
	}
41
42
	encType, err := strconv.Atoi(parts[0])
43
	if err != nil {
44
		return nil, fmt.Errorf("invalid encryption type: %w", err)
45
	}
46
47
	cs := &CipherString{Type: EncType(encType)}
48
	dataParts := strings.Split(parts[1], "|")
49
50
	switch cs.Type {
51
	case EncAesCbc256_B64:
52
		if len(dataParts) != 2 {
53
			return nil, fmt.Errorf("type 0 expects 2 parts (iv|ct), got %d", len(dataParts))
54
		}
55
		cs.IV, err = base64.StdEncoding.DecodeString(dataParts[0])
56
		if err != nil {
57
			return nil, fmt.Errorf("decode IV: %w", err)
58
		}
59
		cs.CT, err = base64.StdEncoding.DecodeString(dataParts[1])
60
		if err != nil {
61
			return nil, fmt.Errorf("decode CT: %w", err)
62
		}
63
64
	case EncAesCbc128_HmacSha256_B64, EncAesCbc256_HmacSha256_B64:
65
		if len(dataParts) != 3 {
66
			return nil, fmt.Errorf("type %d expects 3 parts (iv|ct|mac), got %d", cs.Type, len(dataParts))
67
		}
68
		cs.IV, err = base64.StdEncoding.DecodeString(dataParts[0])
69
		if err != nil {
70
			return nil, fmt.Errorf("decode IV: %w", err)
71
		}
72
		cs.CT, err = base64.StdEncoding.DecodeString(dataParts[1])
73
		if err != nil {
74
			return nil, fmt.Errorf("decode CT: %w", err)
75
		}
76
		cs.MAC, err = base64.StdEncoding.DecodeString(dataParts[2])
77
		if err != nil {
78
			return nil, fmt.Errorf("decode MAC: %w", err)
79
		}
80
81
	case EncRsa2048_OaepSha256_B64, EncRsa2048_OaepSha1_B64:
82
		if len(dataParts) != 1 {
83
			return nil, fmt.Errorf("RSA type expects 1 part (ct), got %d", len(dataParts))
84
		}
85
		cs.CT, err = base64.StdEncoding.DecodeString(dataParts[0])
86
		if err != nil {
87
			return nil, fmt.Errorf("decode CT: %w", err)
88
		}
89
90
	case EncRsa2048_OaepSha256_HmacSha256_B64, EncRsa2048_OaepSha1_HmacSha256_B64:
91
		if len(dataParts) != 2 {
92
			return nil, fmt.Errorf("RSA+HMAC type expects 2 parts (ct|mac), got %d", len(dataParts))
93
		}
94
		cs.CT, err = base64.StdEncoding.DecodeString(dataParts[0])
95
		if err != nil {
96
			return nil, fmt.Errorf("decode CT: %w", err)
97
		}
98
		cs.MAC, err = base64.StdEncoding.DecodeString(dataParts[1])
99
		if err != nil {
100
			return nil, fmt.Errorf("decode MAC: %w", err)
101
		}
102
103
	default:
104
		return nil, fmt.Errorf("unsupported encryption type: %d", cs.Type)
105
	}
106
107
	return cs, nil
108
}
109
110
// MarshalRaw serializes the CipherString to raw binary format (used for file encryption).
111
// Format: 1 byte type | IV | MAC | CT
112
func (cs *CipherString) MarshalRaw() []byte {
113
	var buf []byte
114
	buf = append(buf, byte(cs.Type))
115
	if cs.IV != nil {
116
		buf = append(buf, cs.IV...)
117
	}
118
	if cs.MAC != nil {
119
		buf = append(buf, cs.MAC...)
120
	}
121
	buf = append(buf, cs.CT...)
122
	return buf
123
}
124
125
// ParseRawCipherString parses a raw binary CipherString (used for encrypted file content).
126
// Format: 1 byte type | IV (16 bytes) | MAC (32 bytes) | CT (rest)
127
func ParseRawCipherString(data []byte) (*CipherString, error) {
128
	if len(data) < 1 {
129
		return nil, fmt.Errorf("raw cipher string too short")
130
	}
131
132
	cs := &CipherString{Type: EncType(data[0])}
133
	rest := data[1:]
134
135
	switch cs.Type {
136
	case EncAesCbc256_HmacSha256_B64, EncAesCbc128_HmacSha256_B64:
137
		// IV (16) + MAC (32) + CT (rest)
138
		if len(rest) < 48+16 { // at least 16 IV + 32 MAC + 16 CT (one block)
139
			return nil, fmt.Errorf("raw cipher data too short for type %d: %d bytes", cs.Type, len(rest))
140
		}
141
		cs.IV = rest[:16]
142
		cs.MAC = rest[16:48]
143
		cs.CT = rest[48:]
144
	case EncAesCbc256_B64:
145
		// IV (16) + CT (rest)
146
		if len(rest) < 32 { // at least 16 IV + 16 CT
147
			return nil, fmt.Errorf("raw cipher data too short for type %d: %d bytes", cs.Type, len(rest))
148
		}
149
		cs.IV = rest[:16]
150
		cs.CT = rest[16:]
151
	default:
152
		return nil, fmt.Errorf("unsupported raw cipher type: %d", cs.Type)
153
	}
154
155
	return cs, nil
156
}
157
158
// String serializes the CipherString back to its wire format.
159
func (cs *CipherString) String() string {
160
	switch cs.Type {
161
	case EncAesCbc256_B64:
162
		return fmt.Sprintf("%d.%s|%s",
163
			cs.Type,
164
			base64.StdEncoding.EncodeToString(cs.IV),
165
			base64.StdEncoding.EncodeToString(cs.CT),
166
		)
167
	case EncAesCbc128_HmacSha256_B64, EncAesCbc256_HmacSha256_B64:
168
		return fmt.Sprintf("%d.%s|%s|%s",
169
			cs.Type,
170
			base64.StdEncoding.EncodeToString(cs.IV),
171
			base64.StdEncoding.EncodeToString(cs.CT),
172
			base64.StdEncoding.EncodeToString(cs.MAC),
173
		)
174
	case EncRsa2048_OaepSha256_B64, EncRsa2048_OaepSha1_B64:
175
		return fmt.Sprintf("%d.%s",
176
			cs.Type,
177
			base64.StdEncoding.EncodeToString(cs.CT),
178
		)
179
	case EncRsa2048_OaepSha256_HmacSha256_B64, EncRsa2048_OaepSha1_HmacSha256_B64:
180
		return fmt.Sprintf("%d.%s|%s",
181
			cs.Type,
182
			base64.StdEncoding.EncodeToString(cs.CT),
183
			base64.StdEncoding.EncodeToString(cs.MAC),
184
		)
185
	default:
186
		return ""
187
	}
188
}
189

Source Files