Skip to content

Commit 7f2ef9f

Browse files
authored
Merge pull request #40 from keybase/surya/CORE-10499/secretservice
d-bus secretservice support
2 parents da9bb48 + e7ef407 commit 7f2ef9f

47 files changed

Lines changed: 8030 additions & 6 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.travis.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
language: go
22

3-
os: osx
3+
os:
4+
- osx
5+
- linux
46

57
before_install:
68
- go get golang.org/x/lint/golint
79

810
script:
911
- go vet ./...
1012
- golint ./...
11-
- go test ./...
13+
- go test -tags skipsecretserviceintegrationtests ./...
1214

1315
go:
14-
- 1.9.x
1516
- 1.10.x
1617
- 1.11.x
1718
- master

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# Go Keychain
22

3-
A library for accessing the Keychain for macOS and iOS in Go (golang).
3+
A library for accessing the Keychain for macOS, iOS, and Linux in Go (golang).
44

5-
Requires macOS 10.9 or greater and iOS 8 or greater.
5+
Requires macOS 10.9 or greater and iOS 8 or greater. On Linux, communicates to
6+
a provider of the DBUS SecretService spec like gnome-keyring or ksecretservice.
67

78
```go
89
import "github.com/keybase/go-keychain"
910
```
1011

1112

12-
## Usage
13+
## Mac/iOS Usage
1314

1415
The API is meant to mirror the macOS/iOS Keychain API and is not necessarily idiomatic go.
1516

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// This file implements a basic Diffie-Hellman for groups with modular
2+
// exponentiation operators. In particular, it is used in this package
3+
// to implement the Diffie-Hellman KEX over the Second Oakley Group.
4+
// meant only for use for securing the channel to the D-Bus Secret Service.
5+
// Much of the code in this file is derived from package
6+
// golang.org/x/crypto/ssh:kex.go, and is replicated here because the relevant
7+
// variables and methods are not exported or easily accessible.
8+
// Note that this protocol is NOT authenticated, NOT secure against malleation
9+
// and is NOT CCA2-secure. It is only meant to hide the D-Bus messages from any
10+
// system services that may be logging everything.
11+
12+
package secretservice
13+
14+
import (
15+
"bytes"
16+
"crypto/aes"
17+
"crypto/cipher"
18+
cryptorand "crypto/rand"
19+
"crypto/sha256"
20+
"fmt"
21+
"io"
22+
"math/big"
23+
24+
errors "github.com/pkg/errors"
25+
"golang.org/x/crypto/hkdf"
26+
)
27+
28+
type dhGroup struct {
29+
g, p, pMinus1 *big.Int
30+
}
31+
32+
var bigOne = big.NewInt(1)
33+
34+
func (group *dhGroup) NewKeypair() (private *big.Int, public *big.Int, err error) {
35+
for {
36+
if private, err = cryptorand.Int(cryptorand.Reader, group.pMinus1); err != nil {
37+
return nil, nil, err
38+
}
39+
if private.Sign() > 0 {
40+
break
41+
}
42+
}
43+
public = new(big.Int).Exp(group.g, private, group.p)
44+
return private, public, nil
45+
}
46+
47+
func (group *dhGroup) diffieHellman(theirPublic, myPrivate *big.Int) (*big.Int, error) {
48+
if theirPublic.Cmp(bigOne) <= 0 || theirPublic.Cmp(group.pMinus1) >= 0 {
49+
return nil, errors.New("ssh: DH parameter out of bounds")
50+
}
51+
return new(big.Int).Exp(theirPublic, myPrivate, group.p), nil
52+
}
53+
54+
func rfc2409SecondOakleyGroup() *dhGroup {
55+
p, _ := new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF", 16)
56+
return &dhGroup{
57+
g: new(big.Int).SetInt64(2),
58+
p: p,
59+
pMinus1: new(big.Int).Sub(p, bigOne),
60+
}
61+
}
62+
63+
func (group *dhGroup) keygenHKDFSHA256AES128(theirPublic *big.Int, myPrivate *big.Int) ([]byte, error) {
64+
sharedSecret, err := group.diffieHellman(theirPublic, myPrivate)
65+
if err != nil {
66+
return nil, err
67+
}
68+
sharedSecretBytes := sharedSecret.Bytes()
69+
70+
r := hkdf.New(sha256.New, sharedSecretBytes, nil, nil)
71+
72+
aesKey := make([]byte, 16)
73+
_, err = io.ReadFull(r, aesKey)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
return aesKey, nil
79+
}
80+
81+
func unauthenticatedAESCBCEncrypt(unpaddedPlaintext []byte, key []byte) (iv []byte, ciphertext []byte, err error) {
82+
paddedPlaintext := padPKCS7(unpaddedPlaintext, aes.BlockSize)
83+
block, err := aes.NewCipher(key)
84+
if err != nil {
85+
return nil, nil, err
86+
}
87+
ivSize := aes.BlockSize
88+
iv = make([]byte, ivSize)
89+
ciphertext = make([]byte, len(paddedPlaintext))
90+
if _, err := io.ReadFull(cryptorand.Reader, iv); err != nil {
91+
return nil, nil, err
92+
}
93+
mode := cipher.NewCBCEncrypter(block, iv)
94+
mode.CryptBlocks(ciphertext, paddedPlaintext)
95+
return iv, ciphertext, nil
96+
}
97+
98+
func unauthenticatedAESCBCDecrypt(iv []byte, ciphertext []byte, key []byte) ([]byte, error) {
99+
block, err := aes.NewCipher(key)
100+
if err != nil {
101+
return nil, err
102+
}
103+
if len(iv) != aes.BlockSize {
104+
return nil, fmt.Errorf("iv length not aes blocksize")
105+
}
106+
if len(ciphertext) < aes.BlockSize {
107+
return nil, fmt.Errorf("ciphertext smaller than AES block size")
108+
}
109+
if len(ciphertext)%aes.BlockSize != 0 {
110+
return nil, fmt.Errorf("aes ciphertext not a multiple of blocksize")
111+
}
112+
mode := cipher.NewCBCDecrypter(block, iv)
113+
mode.CryptBlocks(ciphertext, ciphertext) // decrypt in-place
114+
plaintext, err := unpadPKCS7(ciphertext, aes.BlockSize)
115+
if err != nil {
116+
return nil, err
117+
}
118+
return plaintext, nil
119+
}
120+
121+
func padPKCS7(xs []byte, n int) []byte {
122+
m := byte(n - (len(xs) % n))
123+
if m == 0 {
124+
m = 16
125+
}
126+
return append(xs, bytes.Repeat([]byte{m}, int(m))...)
127+
}
128+
129+
func unpadPKCS7(xs []byte, n int) ([]byte, error) {
130+
if len(xs) == 0 {
131+
return nil, fmt.Errorf("cannot unpad empty bytearray")
132+
}
133+
if len(xs)%n != 0 {
134+
return nil, fmt.Errorf("length of bytearray not a multiple of blocksize")
135+
}
136+
lastByte := xs[len(xs)-1]
137+
padStartIdx := len(xs) - int(lastByte)
138+
for i := padStartIdx; i < len(xs); i++ {
139+
if xs[i] != lastByte {
140+
return nil, fmt.Errorf("expected pad character %x, got %x", lastByte, xs[i])
141+
}
142+
}
143+
return xs[:padStartIdx], nil
144+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package secretservice
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestNewKeypair(t *testing.T) {
10+
group := rfc2409SecondOakleyGroup()
11+
private, public, err := group.NewKeypair()
12+
require.NoError(t, err)
13+
require.NotNil(t, private)
14+
require.NotNil(t, public)
15+
private2, public2, err := group.NewKeypair()
16+
require.NotEqual(t, private.Cmp(private2), 0, "should get different private key with every keygen")
17+
require.NotEqual(t, public.Cmp(public2), 0, "should get different public key with every keygen")
18+
}
19+
20+
func TestKeygen(t *testing.T) {
21+
group := rfc2409SecondOakleyGroup()
22+
myPrivate, myPublic, err := group.NewKeypair()
23+
require.NoError(t, err)
24+
theirPrivate, theirPublic, err := group.NewKeypair()
25+
require.NoError(t, err)
26+
27+
myKey, err := group.keygenHKDFSHA256AES128(theirPublic, myPrivate)
28+
theirKey, err := group.keygenHKDFSHA256AES128(myPublic, theirPrivate)
29+
require.Equal(t, myKey, theirKey)
30+
}
31+
32+
func TestEncryption(t *testing.T) {
33+
key := []byte("YELLOW SUBMARINE")
34+
plaintext := []byte("hello world")
35+
iv, ciphertext, err := unauthenticatedAESCBCEncrypt(plaintext, key)
36+
require.NoError(t, err)
37+
gotPlaintext, err := unauthenticatedAESCBCDecrypt(iv, ciphertext, key)
38+
require.NoError(t, err)
39+
require.Equal(t, plaintext, gotPlaintext)
40+
}
41+
42+
func TestEncryptionRng(t *testing.T) {
43+
key := []byte("YELLOW SUBMARINE")
44+
plaintext := []byte("hello world")
45+
iv1, ciphertext1, err := unauthenticatedAESCBCEncrypt(plaintext, key)
46+
require.NoError(t, err)
47+
iv2, ciphertext2, err := unauthenticatedAESCBCEncrypt(plaintext, key)
48+
require.NoError(t, err)
49+
require.NotEqual(t, iv1, iv2)
50+
require.NotEqual(t, ciphertext1, ciphertext2)
51+
}
52+
53+
var pkcs7tests = []struct {
54+
in []byte
55+
out []byte
56+
}{
57+
{[]byte{}, []byte{4, 4, 4, 4}},
58+
{[]byte{1, 2}, []byte{1, 2, 2, 2}},
59+
{[]byte{1, 2, 3}, []byte{1, 2, 3, 1}},
60+
{[]byte{1, 2, 3, 4}, []byte{1, 2, 3, 4, 4, 4, 4, 4}},
61+
{[]byte{1, 2, 3, 4, 5}, []byte{1, 2, 3, 4, 5, 3, 3, 3}},
62+
{[]byte{1, 2, 3, 4, 1, 1, 1}, []byte{1, 2, 3, 4, 1, 1, 1, 1}},
63+
}
64+
65+
func TestPKCS7(t *testing.T) {
66+
for _, testCase := range pkcs7tests {
67+
require.Equal(t, padPKCS7(testCase.in, 4), testCase.out)
68+
preimage, err := unpadPKCS7(testCase.out, 4)
69+
require.NoError(t, err)
70+
require.Equal(t, preimage, testCase.in)
71+
}
72+
73+
_, err := unpadPKCS7([]byte{}, 4)
74+
require.Error(t, err)
75+
_, err = unpadPKCS7([]byte{1, 2, 3, 4}, 4)
76+
require.Error(t, err)
77+
_, err = unpadPKCS7([]byte{1, 2, 3, 3}, 4)
78+
require.Error(t, err)
79+
_, err = unpadPKCS7([]byte{1, 2, 3, 4, 1, 1, 1, 2}, 4)
80+
require.Error(t, err)
81+
}

0 commit comments

Comments
 (0)