From 33daaea8c22056c1f66d831864637bbf80e3a5ce Mon Sep 17 00:00:00 2001 From: Michael Tibben Date: Fri, 13 Apr 2018 13:44:06 +1000 Subject: [PATCH 1/2] Update go-keychain for go 1.10 compatability --- .../go-keychain/corefoundation_1.10.go | 359 +++++++++++ ...oundation.go => corefoundation_pre1.10.go} | 17 +- .../{keychain.go => keychain_1.10.go} | 11 +- .../keybase/go-keychain/keychain_pre1.10.go | 562 ++++++++++++++++++ .../keybase/go-keychain/macos_1.10.go | 272 +++++++++ .../{macos.go => macos_pre1.10.go} | 8 +- vendor/vendor.json | 6 +- 7 files changed, 1217 insertions(+), 18 deletions(-) create mode 100644 vendor/github.com/keybase/go-keychain/corefoundation_1.10.go rename vendor/github.com/keybase/go-keychain/{corefoundation.go => corefoundation_pre1.10.go} (96%) rename vendor/github.com/keybase/go-keychain/{keychain.go => keychain_1.10.go} (98%) create mode 100644 vendor/github.com/keybase/go-keychain/keychain_pre1.10.go create mode 100644 vendor/github.com/keybase/go-keychain/macos_1.10.go rename vendor/github.com/keybase/go-keychain/{macos.go => macos_pre1.10.go} (97%) diff --git a/vendor/github.com/keybase/go-keychain/corefoundation_1.10.go b/vendor/github.com/keybase/go-keychain/corefoundation_1.10.go new file mode 100644 index 000000000..23756ee5c --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/corefoundation_1.10.go @@ -0,0 +1,359 @@ +// +build darwin ios +// +build go1.10 + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation + +#include + +// Can't cast a *uintptr to *unsafe.Pointer in Go, and casting +// C.CFTypeRef to unsafe.Pointer is unsafe in Go, so have shim functions to +// do the casting in C (where it's safe). + +// We add a suffix to the C functions below, because we copied this +// file from go-kext, which means that any project that depends on this +// package and go-kext would run into duplicate symbol errors otherwise. +// +// TODO: Move this file into its own package depended on by go-kext +// and this package. + +CFDictionaryRef CFDictionaryCreateSafe2(CFAllocatorRef allocator, const uintptr_t *keys, const uintptr_t *values, CFIndex numValues, const CFDictionaryKeyCallBacks *keyCallBacks, const CFDictionaryValueCallBacks *valueCallBacks) { + return CFDictionaryCreate(allocator, (const void **)keys, (const void **)values, numValues, keyCallBacks, valueCallBacks); +} + +CFArrayRef CFArrayCreateSafe2(CFAllocatorRef allocator, const uintptr_t *values, CFIndex numValues, const CFArrayCallBacks *callBacks) { + return CFArrayCreate(allocator, (const void **)values, numValues, callBacks); +} +*/ +import "C" +import ( + "errors" + "fmt" + "math" + "reflect" + "unicode/utf8" + "unsafe" +) + +// Release releases memory pointed to by a CFTypeRef. +func Release(ref C.CFTypeRef) { + C.CFRelease(ref) +} + +// BytesToCFData will return a CFDataRef and if non-nil, must be released with +// Release(ref). +func BytesToCFData(b []byte) (C.CFDataRef, error) { + if uint64(len(b)) > math.MaxUint32 { + return 0, errors.New("Data is too large") + } + var p *C.UInt8 + if len(b) > 0 { + p = (*C.UInt8)(&b[0]) + } + cfData := C.CFDataCreate(nil, p, C.CFIndex(len(b))) + if cfData == 0 { + return 0, fmt.Errorf("CFDataCreate failed") + } + return cfData, nil +} + +// CFDataToBytes converts CFData to bytes. +func CFDataToBytes(cfData C.CFDataRef) ([]byte, error) { + return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(cfData)), C.int(C.CFDataGetLength(cfData))), nil +} + +// MapToCFDictionary will return a CFDictionaryRef and if non-nil, must be +// released with Release(ref). +func MapToCFDictionary(m map[C.CFTypeRef]C.CFTypeRef) (C.CFDictionaryRef, error) { + var keys, values []C.uintptr_t + for key, value := range m { + keys = append(keys, C.uintptr_t(key)) + values = append(values, C.uintptr_t(value)) + } + numValues := len(values) + var keysPointer, valuesPointer *C.uintptr_t + if numValues > 0 { + keysPointer = &keys[0] + valuesPointer = &values[0] + } + cfDict := C.CFDictionaryCreateSafe2(nil, keysPointer, valuesPointer, C.CFIndex(numValues), &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) + if cfDict == 0 { + return 0, fmt.Errorf("CFDictionaryCreate failed") + } + return cfDict, nil +} + +// CFDictionaryToMap converts CFDictionaryRef to a map. +func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]C.CFTypeRef) { + count := C.CFDictionaryGetCount(cfDict) + if count > 0 { + keys := make([]C.CFTypeRef, count) + values := make([]C.CFTypeRef, count) + C.CFDictionaryGetKeysAndValues(cfDict, (*unsafe.Pointer)(unsafe.Pointer(&keys[0])), (*unsafe.Pointer)(unsafe.Pointer(&values[0]))) + m = make(map[C.CFTypeRef]C.CFTypeRef, count) + for i := C.CFIndex(0); i < count; i++ { + m[keys[i]] = values[i] + } + } + return +} + +// StringToCFString will return a CFStringRef and if non-nil, must be released with +// Release(ref). +func StringToCFString(s string) (C.CFStringRef, error) { + if !utf8.ValidString(s) { + return 0, errors.New("Invalid UTF-8 string") + } + if uint64(len(s)) > math.MaxUint32 { + return 0, errors.New("String is too large") + } + + bytes := []byte(s) + var p *C.UInt8 + if len(bytes) > 0 { + p = (*C.UInt8)(&bytes[0]) + } + return C.CFStringCreateWithBytes(nil, p, C.CFIndex(len(s)), C.kCFStringEncodingUTF8, C.false), nil +} + +// CFStringToString converts a CFStringRef to a string. +func CFStringToString(s C.CFStringRef) string { + p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8) + if p != nil { + return C.GoString(p) + } + length := C.CFStringGetLength(s) + if length == 0 { + return "" + } + maxBufLen := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8) + if maxBufLen == 0 { + return "" + } + buf := make([]byte, maxBufLen) + var usedBufLen C.CFIndex + _ = C.CFStringGetBytes(s, C.CFRange{0, length}, C.kCFStringEncodingUTF8, C.UInt8(0), C.false, (*C.UInt8)(&buf[0]), maxBufLen, &usedBufLen) + return string(buf[:usedBufLen]) +} + +// ArrayToCFArray will return a CFArrayRef and if non-nil, must be released with +// Release(ref). +func ArrayToCFArray(a []C.CFTypeRef) C.CFArrayRef { + var values []C.uintptr_t + for _, value := range a { + values = append(values, C.uintptr_t(value)) + } + numValues := len(values) + var valuesPointer *C.uintptr_t + if numValues > 0 { + valuesPointer = &values[0] + } + return C.CFArrayCreateSafe2(nil, valuesPointer, C.CFIndex(numValues), &C.kCFTypeArrayCallBacks) +} + +// CFArrayToArray converts a CFArrayRef to an array of CFTypes. +func CFArrayToArray(cfArray C.CFArrayRef) (a []C.CFTypeRef) { + count := C.CFArrayGetCount(cfArray) + if count > 0 { + a = make([]C.CFTypeRef, count) + C.CFArrayGetValues(cfArray, C.CFRange{0, count}, (*unsafe.Pointer)(unsafe.Pointer(&a[0]))) + } + return +} + +// Convertable knows how to convert an instance to a CFTypeRef. +type Convertable interface { + Convert() (C.CFTypeRef, error) +} + +// ConvertMapToCFDictionary converts a map to a CFDictionary and if non-nil, +// must be released with Release(ref). +func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, error) { + m := make(map[C.CFTypeRef]C.CFTypeRef) + for key, i := range attr { + var valueRef C.CFTypeRef + switch val := i.(type) { + default: + return 0, fmt.Errorf("Unsupported value type: %v", reflect.TypeOf(i)) + case C.CFTypeRef: + valueRef = val + case bool: + if val { + valueRef = C.CFTypeRef(C.kCFBooleanTrue) + } else { + valueRef = C.CFTypeRef(C.kCFBooleanFalse) + } + case []byte: + bytesRef, err := BytesToCFData(val) + if err != nil { + return 0, err + } + valueRef = C.CFTypeRef(bytesRef) + defer Release(valueRef) + case string: + stringRef, err := StringToCFString(val) + if err != nil { + return 0, err + } + valueRef = C.CFTypeRef(stringRef) + defer Release(valueRef) + case Convertable: + convertedRef, err := val.Convert() + if err != nil { + return 0, err + } + valueRef = C.CFTypeRef(convertedRef) + defer Release(valueRef) + } + keyRef, err := StringToCFString(key) + if err != nil { + return 0, err + } + m[C.CFTypeRef(keyRef)] = valueRef + defer Release(C.CFTypeRef(keyRef)) + } + + cfDict, err := MapToCFDictionary(m) + if err != nil { + return 0, err + } + return cfDict, nil +} + +// CFTypeDescription returns type string for CFTypeRef. +func CFTypeDescription(ref C.CFTypeRef) string { + typeID := C.CFGetTypeID(ref) + typeDesc := C.CFCopyTypeIDDescription(typeID) + defer Release(C.CFTypeRef(typeDesc)) + return CFStringToString(typeDesc) +} + +// Convert converts a CFTypeRef to a go instance. +func Convert(ref C.CFTypeRef) (interface{}, error) { + typeID := C.CFGetTypeID(ref) + if typeID == C.CFStringGetTypeID() { + return CFStringToString(C.CFStringRef(ref)), nil + } else if typeID == C.CFDictionaryGetTypeID() { + return ConvertCFDictionary(C.CFDictionaryRef(ref)) + } else if typeID == C.CFArrayGetTypeID() { + arr := CFArrayToArray(C.CFArrayRef(ref)) + results := make([]interface{}, 0, len(arr)) + for _, ref := range arr { + v, err := Convert(ref) + if err != nil { + return nil, err + } + results = append(results, v) + return results, nil + } + } else if typeID == C.CFDataGetTypeID() { + b, err := CFDataToBytes(C.CFDataRef(ref)) + if err != nil { + return nil, err + } + return b, nil + } else if typeID == C.CFNumberGetTypeID() { + return CFNumberToInterface(C.CFNumberRef(ref)), nil + } else if typeID == C.CFBooleanGetTypeID() { + if C.CFBooleanGetValue(C.CFBooleanRef(ref)) != 0 { + return true, nil + } + return false, nil + } + + return nil, fmt.Errorf("Invalid type: %s", CFTypeDescription(ref)) +} + +// ConvertCFDictionary converts a CFDictionary to map (deep). +func ConvertCFDictionary(d C.CFDictionaryRef) (map[interface{}]interface{}, error) { + m := CFDictionaryToMap(C.CFDictionaryRef(d)) + result := make(map[interface{}]interface{}) + + for k, v := range m { + gk, err := Convert(k) + if err != nil { + return nil, err + } + gv, err := Convert(v) + if err != nil { + return nil, err + } + result[gk] = gv + } + return result, nil +} + +// CFNumberToInterface converts the CFNumberRef to the most appropriate numeric +// type. +// This code is from github.com/kballard/go-osx-plist. +func CFNumberToInterface(cfNumber C.CFNumberRef) interface{} { + typ := C.CFNumberGetType(cfNumber) + switch typ { + case C.kCFNumberSInt8Type: + var sint C.SInt8 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) + return int8(sint) + case C.kCFNumberSInt16Type: + var sint C.SInt16 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) + return int16(sint) + case C.kCFNumberSInt32Type: + var sint C.SInt32 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) + return int32(sint) + case C.kCFNumberSInt64Type: + var sint C.SInt64 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) + return int64(sint) + case C.kCFNumberFloat32Type: + var float C.Float32 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) + return float32(float) + case C.kCFNumberFloat64Type: + var float C.Float64 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) + return float64(float) + case C.kCFNumberCharType: + var char C.char + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&char)) + return byte(char) + case C.kCFNumberShortType: + var short C.short + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&short)) + return int16(short) + case C.kCFNumberIntType: + var i C.int + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&i)) + return int32(i) + case C.kCFNumberLongType: + var long C.long + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&long)) + return int(long) + case C.kCFNumberLongLongType: + // This is the only type that may actually overflow us + var longlong C.longlong + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&longlong)) + return int64(longlong) + case C.kCFNumberFloatType: + var float C.float + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) + return float32(float) + case C.kCFNumberDoubleType: + var double C.double + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&double)) + return float64(double) + case C.kCFNumberCFIndexType: + // CFIndex is a long + var index C.CFIndex + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&index)) + return int(index) + case C.kCFNumberNSIntegerType: + // We don't have a definition of NSInteger, but we know it's either an int or a long + var nsInt C.long + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&nsInt)) + return int(nsInt) + } + panic("Unknown CFNumber type") +} diff --git a/vendor/github.com/keybase/go-keychain/corefoundation.go b/vendor/github.com/keybase/go-keychain/corefoundation_pre1.10.go similarity index 96% rename from vendor/github.com/keybase/go-keychain/corefoundation.go rename to vendor/github.com/keybase/go-keychain/corefoundation_pre1.10.go index 782000d83..ad1786df4 100644 --- a/vendor/github.com/keybase/go-keychain/corefoundation.go +++ b/vendor/github.com/keybase/go-keychain/corefoundation_pre1.10.go @@ -1,4 +1,7 @@ -// +build darwin +// +build darwin ios +// +build !go1.10 + +// TODO: Remove this file once we've completely migrated to go 1.10.x. package keychain @@ -17,7 +20,7 @@ import ( "unsafe" ) -// Release releases a CFTypeRef +// Release releases memory pointed to by a CFTypeRef. func Release(ref C.CFTypeRef) { C.CFRelease(ref) } @@ -66,15 +69,17 @@ func MapToCFDictionary(m map[C.CFTypeRef]C.CFTypeRef) (C.CFDictionaryRef, error) } // CFDictionaryToMap converts CFDictionaryRef to a map. -func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]C.CFTypeRef) { +func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]uintptr) { count := C.CFDictionaryGetCount(cfDict) if count > 0 { keys := make([]C.CFTypeRef, count) values := make([]C.CFTypeRef, count) C.CFDictionaryGetKeysAndValues(cfDict, (*unsafe.Pointer)(&keys[0]), (*unsafe.Pointer)(&values[0])) - m = make(map[C.CFTypeRef]C.CFTypeRef, count) + m = make(map[C.CFTypeRef]uintptr, count) for i := C.CFIndex(0); i < count; i++ { - m[keys[i]] = values[i] + k := keys[i] + v := values[i] + m[k] = uintptr(v) } } return @@ -256,7 +261,7 @@ func ConvertCFDictionary(d C.CFDictionaryRef) (map[interface{}]interface{}, erro if err != nil { return nil, err } - gv, err := Convert(v) + gv, err := Convert(C.CFTypeRef(v)) if err != nil { return nil, err } diff --git a/vendor/github.com/keybase/go-keychain/keychain.go b/vendor/github.com/keybase/go-keychain/keychain_1.10.go similarity index 98% rename from vendor/github.com/keybase/go-keychain/keychain.go rename to vendor/github.com/keybase/go-keychain/keychain_1.10.go index aa800371c..83c33755f 100644 --- a/vendor/github.com/keybase/go-keychain/keychain.go +++ b/vendor/github.com/keybase/go-keychain/keychain_1.10.go @@ -1,4 +1,5 @@ // +build darwin +// +build go1.10 package keychain @@ -335,18 +336,18 @@ type QueryResult struct { func QueryItemRef(item Item) (C.CFTypeRef, error) { cfDict, err := ConvertMapToCFDictionary(item.attr) if err != nil { - return nil, err + return 0, err } defer Release(C.CFTypeRef(cfDict)) var resultsRef C.CFTypeRef errCode := C.SecItemCopyMatching(cfDict, &resultsRef) if Error(errCode) == ErrorItemNotFound { - return nil, nil + return 0, nil } err = checkError(errCode) if err != nil { - return nil, err + return 0, err } return resultsRef, nil } @@ -357,7 +358,7 @@ func QueryItem(item Item) ([]QueryResult, error) { if err != nil { return nil, err } - if resultsRef == nil { + if resultsRef == 0 { return nil, nil } defer Release(resultsRef) @@ -376,7 +377,7 @@ func QueryItem(item Item) ([]QueryResult, error) { } results = append(results, *item) } else { - return nil, fmt.Errorf("Invalid result type (If you SetReturnRef(true) you should use QueryItemRef directly).") + return nil, fmt.Errorf("invalid result type (If you SetReturnRef(true) you should use QueryItemRef directly)") } } } else if typeID == C.CFDictionaryGetTypeID() { diff --git a/vendor/github.com/keybase/go-keychain/keychain_pre1.10.go b/vendor/github.com/keybase/go-keychain/keychain_pre1.10.go new file mode 100644 index 000000000..1d261e9aa --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/keychain_pre1.10.go @@ -0,0 +1,562 @@ +// +build darwin +// +build !go1.10 + +// TODO: Remove this file once we've completely migrated to go 1.10.x. + +package keychain + +// See https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/index.html for the APIs used below. + +// Also see https://developer.apple.com/library/ios/documentation/Security/Conceptual/keychainServConcepts/01introduction/introduction.html . + +/* +#cgo LDFLAGS: -framework CoreFoundation -framework Security + +#include +#include + +// Casting C.CFTypeRef to unsafe.Pointer is unsafe in Go, so have shim +// functions to do the casting in C (where it's safe). + +const UInt8 * CFDataGetBytePtrSafe(uintptr_t theData) { + return CFDataGetBytePtr((CFDataRef)theData); +} + +CFIndex CFDataGetLengthSafe(uintptr_t theData) { + return CFDataGetLength((CFDataRef)theData); +} + +const char * CFStringGetCStringPtrSafe(uintptr_t theString, CFStringEncoding encoding) { + return CFStringGetCStringPtr((CFStringRef)theString, encoding); +} + +CFIndex CFStringGetLengthSafe(uintptr_t theString) { + return CFStringGetLength((CFStringRef)theString); +} + +CFIndex CFStringGetBytesSafe(uintptr_t theString, CFRange range, CFStringEncoding encoding, UInt8 lossByte, Boolean isExternalRepresentation, UInt8 *buffer, CFIndex maxBufLen, CFIndex *usedBufLen) { + return CFStringGetBytes((CFStringRef)theString, range, encoding, lossByte, isExternalRepresentation, buffer, maxBufLen, usedBufLen); +} +*/ +import "C" +import ( + "fmt" + "unsafe" +) + +// Error defines keychain errors +type Error int + +var ( + // ErrorUnimplemented corresponds to errSecUnimplemented result code + ErrorUnimplemented = Error(C.errSecUnimplemented) + // ErrorParam corresponds to errSecParam result code + ErrorParam = Error(C.errSecParam) + // ErrorAllocate corresponds to errSecAllocate result code + ErrorAllocate = Error(C.errSecAllocate) + // ErrorNotAvailable corresponds to errSecNotAvailable result code + ErrorNotAvailable = Error(C.errSecNotAvailable) + // ErrorAuthFailed corresponds to errSecAuthFailed result code + ErrorAuthFailed = Error(C.errSecAuthFailed) + // ErrorDuplicateItem corresponds to errSecDuplicateItem result code + ErrorDuplicateItem = Error(C.errSecDuplicateItem) + // ErrorItemNotFound corresponds to errSecItemNotFound result code + ErrorItemNotFound = Error(C.errSecItemNotFound) + // ErrorInteractionNotAllowed corresponds to errSecInteractionNotAllowed result code + ErrorInteractionNotAllowed = Error(C.errSecInteractionNotAllowed) + // ErrorDecode corresponds to errSecDecode result code + ErrorDecode = Error(C.errSecDecode) + // ErrorNoSuchKeychain corresponds to errSecNoSuchKeychain result code + ErrorNoSuchKeychain = Error(C.errSecNoSuchKeychain) +) + +func checkError(errCode C.OSStatus) error { + if errCode == C.errSecSuccess { + return nil + } + return Error(errCode) +} + +func (k Error) Error() string { + var msg string + // SecCopyErrorMessageString is only available on OSX, so derive manually. + switch k { + case ErrorItemNotFound: + msg = fmt.Sprintf("Item not found (%d)", k) + case ErrorDuplicateItem: + msg = fmt.Sprintf("Duplicate item (%d)", k) + case ErrorParam: + msg = fmt.Sprintf("One or more parameters passed to the function were not valid (%d)", k) + case ErrorNoSuchKeychain: + msg = fmt.Sprintf("No such keychain (%d)", k) + case -25243: + msg = fmt.Sprintf("No access for item (%d)", k) + default: + msg = fmt.Sprintf("Keychain Error (%d)", k) + } + return msg +} + +// SecClass is the items class code +type SecClass int + +// Keychain Item Classes +var ( + /* + kSecClassGenericPassword item attributes: + kSecAttrAccess (OS X only) + kSecAttrAccessGroup (iOS; also OS X if kSecAttrSynchronizable specified) + kSecAttrAccessible (iOS; also OS X if kSecAttrSynchronizable specified) + kSecAttrAccount + kSecAttrService + */ + SecClassGenericPassword SecClass = 1 +) + +// SecClassKey is the key type for SecClass +var SecClassKey = attrKey(C.CFTypeRef(C.kSecClass)) +var secClassTypeRef = map[SecClass]C.CFTypeRef{ + SecClassGenericPassword: C.CFTypeRef(C.kSecClassGenericPassword), +} + +var ( + // ServiceKey is for kSecAttrService + ServiceKey = attrKey(C.CFTypeRef(C.kSecAttrService)) + // LabelKey is for kSecAttrLabel + LabelKey = attrKey(C.CFTypeRef(C.kSecAttrLabel)) + // AccountKey is for kSecAttrAccount + AccountKey = attrKey(C.CFTypeRef(C.kSecAttrAccount)) + // AccessGroupKey is for kSecAttrAccessGroup + AccessGroupKey = attrKey(C.CFTypeRef(C.kSecAttrAccessGroup)) + // DataKey is for kSecValueData + DataKey = attrKey(C.CFTypeRef(C.kSecValueData)) + // DescriptionKey is for kSecAttrDescription + DescriptionKey = attrKey(C.CFTypeRef(C.kSecAttrDescription)) +) + +// Synchronizable is the items synchronizable status +type Synchronizable int + +const ( + // SynchronizableDefault is the default setting + SynchronizableDefault Synchronizable = 0 + // SynchronizableAny is for kSecAttrSynchronizableAny + SynchronizableAny = 1 + // SynchronizableYes enables synchronization + SynchronizableYes = 2 + // SynchronizableNo disables synchronization + SynchronizableNo = 3 +) + +// SynchronizableKey is the key type for Synchronizable +var SynchronizableKey = attrKey(C.CFTypeRef(C.kSecAttrSynchronizable)) +var syncTypeRef = map[Synchronizable]C.CFTypeRef{ + SynchronizableAny: C.CFTypeRef(C.kSecAttrSynchronizableAny), + SynchronizableYes: C.CFTypeRef(C.kCFBooleanTrue), + SynchronizableNo: C.CFTypeRef(C.kCFBooleanFalse), +} + +// Accessible is the items accessibility +type Accessible int + +const ( + // AccessibleDefault is the default + AccessibleDefault Accessible = 0 + // AccessibleWhenUnlocked is when unlocked + AccessibleWhenUnlocked = 1 + // AccessibleAfterFirstUnlock is after first unlock + AccessibleAfterFirstUnlock = 2 + // AccessibleAlways is always + AccessibleAlways = 3 + // AccessibleWhenPasscodeSetThisDeviceOnly is when passcode is set + AccessibleWhenPasscodeSetThisDeviceOnly = 4 + // AccessibleWhenUnlockedThisDeviceOnly is when unlocked for this device only + AccessibleWhenUnlockedThisDeviceOnly = 5 + // AccessibleAfterFirstUnlockThisDeviceOnly is after first unlock for this device only + AccessibleAfterFirstUnlockThisDeviceOnly = 6 + // AccessibleAccessibleAlwaysThisDeviceOnly is always for this device only + AccessibleAccessibleAlwaysThisDeviceOnly = 7 +) + +// MatchLimit is whether to limit results on query +type MatchLimit int + +const ( + // MatchLimitDefault is the default + MatchLimitDefault MatchLimit = 0 + // MatchLimitOne limits to one result + MatchLimitOne = 1 + // MatchLimitAll is no limit + MatchLimitAll = 2 +) + +// MatchLimitKey is key type for MatchLimit +var MatchLimitKey = attrKey(C.CFTypeRef(C.kSecMatchLimit)) +var matchTypeRef = map[MatchLimit]C.CFTypeRef{ + MatchLimitOne: C.CFTypeRef(C.kSecMatchLimitOne), + MatchLimitAll: C.CFTypeRef(C.kSecMatchLimitAll), +} + +// ReturnAttributesKey is key type for kSecReturnAttributes +var ReturnAttributesKey = attrKey(C.CFTypeRef(C.kSecReturnAttributes)) + +// ReturnDataKey is key type for kSecReturnData +var ReturnDataKey = attrKey(C.CFTypeRef(C.kSecReturnData)) + +// ReturnRefKey is key type for kSecReturnRef +var ReturnRefKey = attrKey(C.CFTypeRef(C.kSecReturnRef)) + +// Item for adding, querying or deleting. +type Item struct { + // Values can be string, []byte, Convertable or CFTypeRef (constant). + attr map[string]interface{} +} + +// SetSecClass sets the security class +func (k *Item) SetSecClass(sc SecClass) { + k.attr[SecClassKey] = secClassTypeRef[sc] +} + +// SetString sets a string attibute for a string key +func (k *Item) SetString(key string, s string) { + if s != "" { + k.attr[key] = s + } else { + delete(k.attr, key) + } +} + +// SetService sets the service attribute +func (k *Item) SetService(s string) { + k.SetString(ServiceKey, s) +} + +// SetAccount sets the account attribute +func (k *Item) SetAccount(a string) { + k.SetString(AccountKey, a) +} + +// SetLabel sets the label attribute +func (k *Item) SetLabel(l string) { + k.SetString(LabelKey, l) +} + +// SetDescription sets the description attribute +func (k *Item) SetDescription(s string) { + k.SetString(DescriptionKey, s) +} + +// SetData sets the data attribute +func (k *Item) SetData(b []byte) { + if b != nil { + k.attr[DataKey] = b + } else { + delete(k.attr, DataKey) + } +} + +// SetAccessGroup sets the access group attribute +func (k *Item) SetAccessGroup(ag string) { + k.SetString(AccessGroupKey, ag) +} + +// SetSynchronizable sets the synchronizable attribute +func (k *Item) SetSynchronizable(sync Synchronizable) { + if sync != SynchronizableDefault { + k.attr[SynchronizableKey] = syncTypeRef[sync] + } else { + delete(k.attr, SynchronizableKey) + } +} + +// SetAccessible sets the accessible attribute +func (k *Item) SetAccessible(accessible Accessible) { + if accessible != AccessibleDefault { + k.attr[AccessibleKey] = accessibleTypeRef[accessible] + } else { + delete(k.attr, AccessibleKey) + } +} + +// SetMatchLimit sets the match limit +func (k *Item) SetMatchLimit(matchLimit MatchLimit) { + if matchLimit != MatchLimitDefault { + k.attr[MatchLimitKey] = matchTypeRef[matchLimit] + } else { + delete(k.attr, MatchLimitKey) + } +} + +// SetReturnAttributes sets the return value type on query +func (k *Item) SetReturnAttributes(b bool) { + k.attr[ReturnAttributesKey] = b +} + +// SetReturnData enables returning data on query +func (k *Item) SetReturnData(b bool) { + k.attr[ReturnDataKey] = b +} + +// SetReturnRef enables returning references on query +func (k *Item) SetReturnRef(b bool) { + k.attr[ReturnRefKey] = b +} + +// NewItem is a new empty keychain item +func NewItem() Item { + return Item{make(map[string]interface{})} +} + +// NewGenericPassword creates a generic password item with the default keychain. This is a convenience method. +func NewGenericPassword(service string, account string, label string, data []byte, accessGroup string) Item { + item := NewItem() + item.SetSecClass(SecClassGenericPassword) + item.SetService(service) + item.SetAccount(account) + item.SetLabel(label) + item.SetData(data) + item.SetAccessGroup(accessGroup) + return item +} + +// AddItem adds a Item to a Keychain +func AddItem(item Item) error { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + + errCode := C.SecItemAdd(cfDict, nil) + err = checkError(errCode) + return err +} + +// UpdateItem updates the queryItem with the parameters from updateItem +func UpdateItem(queryItem Item, updateItem Item) error { + cfDict, err := ConvertMapToCFDictionary(queryItem.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + cfDictUpdate, err := ConvertMapToCFDictionary(updateItem.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDictUpdate)) + errCode := C.SecItemUpdate(cfDict, cfDictUpdate) + err = checkError(errCode) + return err +} + +// QueryResult stores all possible results from queries. +// Not all fields are applicable all the time. Results depend on query. +type QueryResult struct { + Service string + Account string + AccessGroup string + Label string + Description string + Data []byte +} + +// QueryItemRef returns query result as CFTypeRef. You must release it when you are done. +func QueryItemRef(item Item) (C.CFTypeRef, error) { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return nil, err + } + defer Release(C.CFTypeRef(cfDict)) + + var resultsRef C.CFTypeRef + errCode := C.SecItemCopyMatching(cfDict, &resultsRef) + if Error(errCode) == ErrorItemNotFound { + return nil, nil + } + err = checkError(errCode) + if err != nil { + return nil, err + } + return resultsRef, nil +} + +// QueryItem returns a list of query results. +func QueryItem(item Item) ([]QueryResult, error) { + resultsRef, err := QueryItemRef(item) + if err != nil { + return nil, err + } + if resultsRef == nil { + return nil, nil + } + defer Release(resultsRef) + + results := make([]QueryResult, 0, 1) + + typeID := C.CFGetTypeID(resultsRef) + if typeID == C.CFArrayGetTypeID() { + arr := CFArrayToArray(C.CFArrayRef(resultsRef)) + for _, ref := range arr { + elementTypeID := C.CFGetTypeID(ref) + if elementTypeID == C.CFDictionaryGetTypeID() { + item, err := convertResult(C.CFDictionaryRef(ref)) + if err != nil { + return nil, err + } + results = append(results, *item) + } else { + return nil, fmt.Errorf("invalid result type (If you SetReturnRef(true) you should use QueryItemRef directly)") + } + } + } else if typeID == C.CFDictionaryGetTypeID() { + item, err := convertResult(C.CFDictionaryRef(resultsRef)) + if err != nil { + return nil, err + } + results = append(results, *item) + } else if typeID == C.CFDataGetTypeID() { + b, err := CFDataToBytes(C.CFDataRef(resultsRef)) + if err != nil { + return nil, err + } + item := QueryResult{Data: b} + results = append(results, item) + } else { + return nil, fmt.Errorf("Invalid result type: %s", CFTypeDescription(resultsRef)) + } + + return results, nil +} + +func attrKey(ref C.CFTypeRef) string { + return CFStringToString(C.CFStringRef(ref)) +} + +// uintptrCFDataToBytes converts a uintptr (assumed to have been +// converted from a CFDataRef) to bytes. +// +// This is an adaptation of CFDataToBytes. +func uintptrCFDataToBytes(cfData uintptr) ([]byte, error) { + return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtrSafe(C.uintptr_t(cfData))), C.int(C.CFDataGetLengthSafe(C.uintptr_t(cfData)))), nil +} + +// uintptrCFStringToString converts a uintptr (assumed to have been +// converted from a CFStringRef) to a string. +// +// This is an adaptation of CFStringToString. +func uintptrCFStringToString(s uintptr) string { + p := C.CFStringGetCStringPtrSafe(C.uintptr_t(s), C.kCFStringEncodingUTF8) + if p != nil { + return C.GoString(p) + } + length := C.CFStringGetLengthSafe(C.uintptr_t(s)) + if length == 0 { + return "" + } + maxBufLen := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8) + if maxBufLen == 0 { + return "" + } + buf := make([]byte, maxBufLen) + var usedBufLen C.CFIndex + _ = C.CFStringGetBytesSafe(C.uintptr_t(s), C.CFRange{0, length}, C.kCFStringEncodingUTF8, C.UInt8(0), C.false, (*C.UInt8)(&buf[0]), maxBufLen, &usedBufLen) + return string(buf[:usedBufLen]) +} + +func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { + m := CFDictionaryToMap(C.CFDictionaryRef(d)) + result := QueryResult{} + for k, v := range m { + switch attrKey(k) { + case ServiceKey: + result.Service = uintptrCFStringToString(v) + case AccountKey: + result.Account = uintptrCFStringToString(v) + case AccessGroupKey: + result.AccessGroup = uintptrCFStringToString(v) + case LabelKey: + result.Label = uintptrCFStringToString(v) + case DescriptionKey: + result.Description = uintptrCFStringToString(v) + case DataKey: + b, err := uintptrCFDataToBytes(v) + if err != nil { + return nil, err + } + result.Data = b + // default: + // fmt.Printf("Unhandled key in conversion: %v = %v\n", cfTypeValue(k), cfTypeValue(p)) + } + } + return &result, nil +} + +// DeleteGenericPasswordItem removes a generic password item. +func DeleteGenericPasswordItem(service string, account string) error { + item := NewItem() + item.SetSecClass(SecClassGenericPassword) + item.SetService(service) + item.SetAccount(account) + return DeleteItem(item) +} + +// DeleteItem removes a Item +func DeleteItem(item Item) error { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + + errCode := C.SecItemDelete(cfDict) + return checkError(errCode) +} + +// GetAccountsForService is deprecated +func GetAccountsForService(service string) ([]string, error) { + return GetGenericPasswordAccounts(service) +} + +// GetGenericPasswordAccounts returns generic password accounts for service. This is a convenience method. +func GetGenericPasswordAccounts(service string) ([]string, error) { + query := NewItem() + query.SetSecClass(SecClassGenericPassword) + query.SetService(service) + query.SetMatchLimit(MatchLimitAll) + query.SetReturnAttributes(true) + results, err := QueryItem(query) + if err != nil { + return nil, err + } + + accounts := make([]string, 0, len(results)) + for _, r := range results { + accounts = append(accounts, r.Account) + } + + return accounts, nil +} + +// GetGenericPassword returns password data for service and account. This is a convenience method. +// If item is not found returns nil, nil. +func GetGenericPassword(service string, account string, label string, accessGroup string) ([]byte, error) { + query := NewItem() + query.SetSecClass(SecClassGenericPassword) + query.SetService(service) + query.SetAccount(account) + query.SetLabel(label) + query.SetAccessGroup(accessGroup) + query.SetMatchLimit(MatchLimitOne) + query.SetReturnData(true) + results, err := QueryItem(query) + if err != nil { + return nil, err + } + if len(results) > 1 { + return nil, fmt.Errorf("Too many results") + } + if len(results) == 1 { + return results[0].Data, nil + } + return nil, nil +} diff --git a/vendor/github.com/keybase/go-keychain/macos_1.10.go b/vendor/github.com/keybase/go-keychain/macos_1.10.go new file mode 100644 index 000000000..69711e192 --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/macos_1.10.go @@ -0,0 +1,272 @@ +// +build darwin,!ios +// +build go1.10 + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation -framework Security + +#include +#include +*/ +import "C" +import ( + "os" + "unsafe" +) + +// AccessibleKey is key for kSecAttrAccessible +var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) +var accessibleTypeRef = map[Accessible]C.CFTypeRef{ + AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), + AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock), + AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways), + AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly), + AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), + AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), + + // Only available in 10.10 + //AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), +} + +var ( + // AccessKey is key for kSecAttrAccess + AccessKey = attrKey(C.CFTypeRef(C.kSecAttrAccess)) +) + +// createAccess creates a SecAccessRef as CFTypeRef. +// The returned SecAccessRef, if non-nil, must be released via CFRelease. +func createAccess(label string, trustedApplications []string) (C.CFTypeRef, error) { + var err error + var labelRef C.CFStringRef + if labelRef, err = StringToCFString(label); err != nil { + return 0, err + } + defer C.CFRelease(C.CFTypeRef(labelRef)) + + var trustedApplicationsArray C.CFArrayRef + if trustedApplications != nil { + if len(trustedApplications) > 0 { + // Always prepend with empty string which signifies that we + // include a NULL application, which means ourselves. + trustedApplications = append([]string{""}, trustedApplications...) + } + + var trustedApplicationsRefs []C.CFTypeRef + for _, trustedApplication := range trustedApplications { + trustedApplicationRef, createErr := createTrustedApplication(trustedApplication) + if createErr != nil { + return 0, createErr + } + defer C.CFRelease(C.CFTypeRef(trustedApplicationRef)) + trustedApplicationsRefs = append(trustedApplicationsRefs, trustedApplicationRef) + } + + trustedApplicationsArray = ArrayToCFArray(trustedApplicationsRefs) + defer C.CFRelease(C.CFTypeRef(trustedApplicationsArray)) + } + + var access C.SecAccessRef + errCode := C.SecAccessCreate(labelRef, trustedApplicationsArray, &access) + err = checkError(errCode) + if err != nil { + return 0, err + } + + return C.CFTypeRef(access), nil +} + +// createTrustedApplication creates a SecTrustedApplicationRef as a CFTypeRef. +// The returned SecTrustedApplicationRef, if non-nil, must be released via CFRelease. +func createTrustedApplication(trustedApplication string) (C.CFTypeRef, error) { + var trustedApplicationCStr *C.char + if trustedApplication != "" { + trustedApplicationCStr = C.CString(trustedApplication) + defer C.free(unsafe.Pointer(trustedApplicationCStr)) + } + + var trustedApplicationRef C.SecTrustedApplicationRef + errCode := C.SecTrustedApplicationCreateFromPath(trustedApplicationCStr, &trustedApplicationRef) + err := checkError(errCode) + if err != nil { + return 0, err + } + + return C.CFTypeRef(trustedApplicationRef), nil +} + +// Access defines whats applications can use the keychain item +type Access struct { + Label string + TrustedApplications []string +} + +// Convert converts Access to CFTypeRef. +// The returned CFTypeRef, if non-nil, must be released via CFRelease. +func (a Access) Convert() (C.CFTypeRef, error) { + return createAccess(a.Label, a.TrustedApplications) +} + +// SetAccess sets Access on Item +func (k *Item) SetAccess(a *Access) { + if a != nil { + k.attr[AccessKey] = a + } else { + delete(k.attr, AccessKey) + } +} + +// DeleteItemRef deletes a keychain item reference. +func DeleteItemRef(ref C.CFTypeRef) error { + errCode := C.SecKeychainItemDelete(C.SecKeychainItemRef(ref)) + return checkError(errCode) +} + +var ( + // KeychainKey is key for kSecUseKeychain + KeychainKey = attrKey(C.CFTypeRef(C.kSecUseKeychain)) + // MatchSearchListKey is key for kSecMatchSearchList + MatchSearchListKey = attrKey(C.CFTypeRef(C.kSecMatchSearchList)) +) + +// Keychain represents the path to a specific OSX keychain +type Keychain struct { + path string +} + +// NewKeychain creates a new keychain file with a password +func NewKeychain(path string, password string) (Keychain, error) { + return newKeychain(path, password, false) +} + +// NewKeychainWithPrompt creates a new Keychain and prompts user for password +func NewKeychainWithPrompt(path string) (Keychain, error) { + return newKeychain(path, "", true) +} + +func newKeychain(path, password string, promptUser bool) (Keychain, error) { + pathRef := C.CString(path) + defer C.free(unsafe.Pointer(pathRef)) + + var errCode C.OSStatus + var kref C.SecKeychainRef + + if promptUser { + errCode = C.SecKeychainCreate(pathRef, C.UInt32(0), nil, C.Boolean(1), 0, &kref) + } else { + passwordRef := C.CString(password) + defer C.free(unsafe.Pointer(passwordRef)) + errCode = C.SecKeychainCreate(pathRef, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(0), 0, &kref) + } + + if err := checkError(errCode); err != nil { + return Keychain{}, err + } + + // TODO: Without passing in kref I get 'One or more parameters passed to the function were not valid (-50)' + defer Release(C.CFTypeRef(kref)) + + return Keychain{ + path: path, + }, nil +} + +// NewWithPath to use an existing keychain +func NewWithPath(path string) Keychain { + return Keychain{ + path: path, + } +} + +// Status returns the status of the keychain +func (kc Keychain) Status() error { + // returns no error even if it doesn't exist + kref, err := openKeychainRef(kc.path) + if err != nil { + return err + } + defer C.CFRelease(C.CFTypeRef(kref)) + + var status C.SecKeychainStatus + return checkError(C.SecKeychainGetStatus(kref, &status)) +} + +// The returned SecKeychainRef, if non-nil, must be released via CFRelease. +func openKeychainRef(path string) (C.SecKeychainRef, error) { + pathName := C.CString(path) + defer C.free(unsafe.Pointer(pathName)) + + var kref C.SecKeychainRef + if err := checkError(C.SecKeychainOpen(pathName, &kref)); err != nil { + return 0, err + } + + return kref, nil +} + +// UnlockAtPath unlocks keychain at path +func UnlockAtPath(path string, password string) error { + kref, err := openKeychainRef(path) + defer Release(C.CFTypeRef(kref)) + if err != nil { + return err + } + passwordRef := C.CString(password) + defer C.free(unsafe.Pointer(passwordRef)) + return checkError(C.SecKeychainUnlock(kref, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(1))) +} + +// LockAtPath locks keychain at path +func LockAtPath(path string) error { + kref, err := openKeychainRef(path) + defer Release(C.CFTypeRef(kref)) + if err != nil { + return err + } + return checkError(C.SecKeychainLock(kref)) +} + +// Delete the Keychain +func (kc *Keychain) Delete() error { + return os.Remove(kc.path) +} + +// Convert Keychain to CFTypeRef. +// The returned CFTypeRef, if non-nil, must be released via CFRelease. +func (kc Keychain) Convert() (C.CFTypeRef, error) { + keyRef, err := openKeychainRef(kc.path) + return C.CFTypeRef(keyRef), err +} + +type keychainArray []Keychain + +// Convert the keychainArray to a CFTypeRef. +// The returned CFTypeRef, if non-nil, must be released via CFRelease. +func (ka keychainArray) Convert() (C.CFTypeRef, error) { + var refs = make([]C.CFTypeRef, len(ka)) + var err error + + for idx, kc := range ka { + if refs[idx], err = kc.Convert(); err != nil { + // If we error trying to convert lets release any we converted before + for _, ref := range refs { + if ref != 0 { + Release(ref) + } + } + return 0, err + } + } + + return C.CFTypeRef(ArrayToCFArray(refs)), nil +} + +// SetMatchSearchList sets match type on keychains +func (k *Item) SetMatchSearchList(karr ...Keychain) { + k.attr[MatchSearchListKey] = keychainArray(karr) +} + +// UseKeychain tells item to use the specified Keychain +func (k *Item) UseKeychain(kc Keychain) { + k.attr[KeychainKey] = kc +} diff --git a/vendor/github.com/keybase/go-keychain/macos.go b/vendor/github.com/keybase/go-keychain/macos_pre1.10.go similarity index 97% rename from vendor/github.com/keybase/go-keychain/macos.go rename to vendor/github.com/keybase/go-keychain/macos_pre1.10.go index 62d4010d0..7990af2ee 100644 --- a/vendor/github.com/keybase/go-keychain/macos.go +++ b/vendor/github.com/keybase/go-keychain/macos_pre1.10.go @@ -1,4 +1,7 @@ // +build darwin,!ios +// +build !go1.10 + +// TODO: Remove this file once we've completely migrated to go 1.10.x. package keychain @@ -212,10 +215,7 @@ func UnlockAtPath(path string, password string) error { } passwordRef := C.CString(password) defer C.free(unsafe.Pointer(passwordRef)) - if err := checkError(C.SecKeychainUnlock(kref, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(1))); err != nil { - return err - } - return nil + return checkError(C.SecKeychainUnlock(kref, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(1))) } // LockAtPath locks keychain at path diff --git a/vendor/vendor.json b/vendor/vendor.json index a8c461c4f..6b023a2fd 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -252,10 +252,10 @@ "revisionTime": "2016-09-29T23:03:56Z" }, { - "checksumSHA1": "U25ZTSzG/RPAXdaPYUlmNXixOWk=", + "checksumSHA1": "9KGH+mjtBmc7T089X/OwDjtypao=", "path": "github.com/keybase/go-keychain", - "revision": "e39a0e22a1cbdbbcdc24b8556df51019c30df068", - "revisionTime": "2017-11-27T20:25:45Z" + "revision": "70b98e9c8d754db775c5bd3b2de3670cc76f1825", + "revisionTime": "2018-03-06T22:15:04Z" }, { "checksumSHA1": "AXacfEchaUqT5RGmPmMXsOWRhv8=", From f0f3e6769f19fa8ad65c9db0d9e444d407c80a60 Mon Sep 17 00:00:00 2001 From: Michael Tibben Date: Fri, 13 Apr 2018 13:50:23 +1000 Subject: [PATCH 2/2] Add go 1.10 to travis config --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9c5de100f..30f50f14c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,9 @@ install: - govendor status go: - - 1.8 - - 1.9 + - "1.8" + - "1.9" + - "1.10" os: - linux