Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/arran4/go-rfc5849-hmac

go 1.24.3
97 changes: 73 additions & 24 deletions gosm.go
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
package go_rfc5849_hmac

import (
"net/http"
"net/url"
"encoding/base64"
"strings"
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"time"
"strconv"
"encoding/base64"
"encoding/hex"
"crypto/rand"
"sort"
"mime"
"bytes"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
)

var (
// PublicKey is the OAuth Consumer Key.
PublicKey = ""
// SecretKey is the OAuth Consumer Secret.
SecretKey = ""
// Token is the OAuth Token Secret.
Token = ""
// TimestampGenerator generates the timestamp for the request.
TimestampGenerator StringResult = DefaultTimestampGenerator
// NonceGenerator generates the nonce for the request.
NonceGenerator StringResult = DefaultNonceGenerator
)

type StringResult func () string
// StringResult is a function that returns a string.
type StringResult func() string

// Pair represents a key-value pair.
type Pair []string

// ParamArray is a collection of Pairs, sortable by key.
type ParamArray []Pair

func (p ParamArray) Len() int {
Expand All @@ -41,23 +50,25 @@ func (p ParamArray) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}

// EncodeBytes encodes the parameters into a query string format according to RFC 5849.
func (array ParamArray) EncodeBytes() []byte {
if array == nil {
return []byte("")
}
var buf bytes.Buffer
sort.Sort(array)
for _, k := range array {
prefix := url.QueryEscape(k[0]) + "="
prefix := Escape(k[0]) + "="
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(prefix)
buf.WriteString(url.QueryEscape(k[1]))
buf.WriteString(Escape(k[1]))
}
return buf.Bytes()
}

// AuthBytes encodes the parameters into the Authorization header format.
func (array ParamArray) AuthBytes() []byte {
if array == nil {
return []byte("")
Expand All @@ -69,16 +80,17 @@ func (array ParamArray) AuthBytes() []byte {
}
sort.Sort(array)
for _, k := range array {
prefix := url.QueryEscape(k[0]) + "="
prefix := Escape(k[0]) + "="
if buf.Len() > 0 {
buf.WriteByte(',')
}
buf.WriteString(prefix)
buf.WriteString("\"" + url.QueryEscape(k[1]) + "\"")
buf.WriteString("\"" + Escape(k[1]) + "\"")
}
return buf.Bytes()
}

// DefaultNonceGenerator returns a random 16-byte hex string.
func DefaultNonceGenerator() string {
nb := make([]byte, 16)
if _, err := rand.Read(nb); err != nil {
Expand All @@ -87,21 +99,24 @@ func DefaultNonceGenerator() string {
return hex.EncodeToString(nb)
}

// DefaultTimestampGenerator returns the current unix timestamp as a string.
func DefaultTimestampGenerator() string {
return strconv.Itoa(int(time.Now().Unix()))
}

// SignSha1Hmac1 signs the request using HMAC-SHA1.
// It adds the Authorization header to the request.
func SignSha1Hmac1(req *http.Request, body string) error {
ts := TimestampGenerator()
nonce := NonceGenerator()
h := hmac.New(sha1.New, []byte(strings.Join([]string{ url.QueryEscape(SecretKey), url.QueryEscape(Token) }, "&")))
h := hmac.New(sha1.New, []byte(strings.Join([]string{Escape(SecretKey), Escape(Token)}, "&")))

authorizationParams := []Pair{
Pair{"oauth_consumer_key", PublicKey},
Pair{"oauth_nonce",nonce,},
Pair{"oauth_signature_method","HMAC-SHA1",},
Pair{"oauth_timestamp",ts,},
Pair{"oauth_version","1.0",},
Pair{"oauth_nonce", nonce},
Pair{"oauth_signature_method", "HMAC-SHA1"},
Pair{"oauth_timestamp", ts},
Pair{"oauth_version", "1.0"},
}

var params ParamArray = append([]Pair{}, authorizationParams...)
Expand All @@ -115,15 +130,15 @@ func SignSha1Hmac1(req *http.Request, body string) error {
} else {
for k, vs := range ps {
for _, v := range vs {
params = append(params, Pair{k, v,})
params = append(params, Pair{k, v})
}
}
}

if req.URL.Query() != nil {
for k, vs := range req.URL.Query() {
for _, v := range vs {
params = append(params, Pair{k, v,})
params = append(params, Pair{k, v})
}
}
}
Expand All @@ -132,15 +147,49 @@ func SignSha1Hmac1(req *http.Request, body string) error {
u2.RawQuery = ""

oauthparam := ParamArray(params).EncodeBytes()
signThis := strings.Join([]string{req.Method, url.QueryEscape(u2.String()), url.QueryEscape(string(oauthparam)), },"&")
signThis := strings.Join([]string{req.Method, Escape(u2.String()), Escape(string(oauthparam))}, "&")
h.Write([]byte(signThis))
hb := h.Sum(nil)
signature := base64.StdEncoding.EncodeToString(hb)
authorizationParams = append(authorizationParams, Pair{"oauth_signature", signature,})
authstring := bytes.NewBufferString("OAuth,")
authorizationParams = append(authorizationParams, Pair{"oauth_signature", signature})
authstring := bytes.NewBufferString("OAuth ")

authstring.Write(ParamArray(authorizationParams).AuthBytes())
req.Header.Add("Authorization", authstring.String())

return nil
}

// Escape encodes a string according to RFC 3986.
// It escapes all characters except unreserved characters (ALPHA, DIGIT, "-", ".", "_", "~").
func Escape(s string) string {
t := make([]byte, 0, 3*len(s))
for i := 0; i < len(s); i++ {
c := s[i]
if isUnreserved(c) {
t = append(t, c)
} else {
t = append(t, '%')
t = append(t, "0123456789ABCDEF"[c>>4])
t = append(t, "0123456789ABCDEF"[c&15])
}
}
return string(t)
}

func isUnreserved(c byte) bool {
if 'A' <= c && c <= 'Z' {
return true
}
if 'a' <= c && c <= 'z' {
return true
}
if '0' <= c && c <= '9' {
return true
}
switch c {
case '-', '.', '_', '~':
return true
}
return false
}
92 changes: 90 additions & 2 deletions gosm_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,95 @@
package go_rfc5849_hmac

import "testing"
import (
"net/http"
"strings"
"testing"
)

func TestEncode(t *testing.T) {
func TestParamArray_EncodeBytes(t *testing.T) {
params := ParamArray{
{"a", "1"},
{"c", "3"},
{"b", "2"},
{"space", "a b"},
{"special", "a=b"},
}
expected := "a=1&b=2&c=3&space=a%20b&special=a%3Db"
encoded := string(params.EncodeBytes())
if encoded != expected {
t.Errorf("EncodeBytes() = %v, want %v", encoded, expected)
}
}

func TestParamArray_AuthBytes(t *testing.T) {
params := ParamArray{
{"a", "1"},
{"c", "3"},
{"b", "2"},
}
expected := "a=\"1\",b=\"2\",c=\"3\""
auth := string(params.AuthBytes())
if auth != expected {
t.Errorf("AuthBytes() = %v, want %v", auth, expected)
}
}

func TestSignSha1Hmac1(t *testing.T) {
// Save original generators and restore them after test
origTimestampGen := TimestampGenerator
origNonceGen := NonceGenerator
defer func() {
TimestampGenerator = origTimestampGen
NonceGenerator = origNonceGen
}()

// Mock generators
TimestampGenerator = func() string { return "1234567890" }
NonceGenerator = func() string { return "nonce_value" }

// Set keys
PublicKey = "consumer_key"
SecretKey = "consumer_secret"
Token = "token_value"

u := "http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b"
bodyText := "c2&a3=2+q"
req, err := http.NewRequest("POST", u, strings.NewReader(bodyText))
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

err = SignSha1Hmac1(req, bodyText)
if err != nil {
t.Fatalf("SignSha1Hmac1 failed: %v", err)
}

auth := req.Header.Get("Authorization")
// Expected signature based on previous manual check (updated for OAuth space fix)
// Base String: POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_key%3Dconsumer_key%26oauth_nonce%3Dnonce_value%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1234567890%26oauth_version%3D1.0
// Key: consumer_secret&token_value
// Signature: ws+3QTlNmh+txA7FTGVCdt9XwKk=
// Encoded Signature: ws%2B3QTlNmh%2BtxA7FTGVCdt9XwKk%3D

expectedStart := "OAuth oauth_consumer_key=\"consumer_key\",oauth_nonce=\"nonce_value\",oauth_signature=\"ws%2B3QTlNmh%2BtxA7FTGVCdt9XwKk%3D\""

if !strings.HasPrefix(auth, expectedStart) {
t.Errorf("Authorization header does not start with expected value.\nGot: %s\nWant prefix: %s", auth, expectedStart)
}

expectedParts := []string{
"oauth_consumer_key=\"consumer_key\"",
"oauth_nonce=\"nonce_value\"",
"oauth_signature_method=\"HMAC-SHA1\"",
"oauth_timestamp=\"1234567890\"",
"oauth_version=\"1.0\"",
"oauth_signature=\"ws%2B3QTlNmh%2BtxA7FTGVCdt9XwKk%3D\"",
}

for _, part := range expectedParts {
if !strings.Contains(auth, part) {
t.Errorf("Authorization header missing part: %s", part)
}
}
}
43 changes: 28 additions & 15 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,50 @@
A very basic simple RFC5849 SHA1 HMAC implementation.
# go-rfc5849-hmac

Usage:
A very basic simple RFC5849 (OAuth 1.0a) SHA1 HMAC implementation in Go.

```
## Usage

```go
package main

import (
"fmt"
"log"
"net/http"
"strings"
"log"
"bytes"
"github.com/arran4/go-rfc5849-hmac"

go_rfc5849_hmac "github.com/arran4/go-rfc5849-hmac"
)

func main() {
u := "https://localhost/api/?format=json"
bodyText :=
u := "https://example.com/api/?format=json"
bodyText := `{"key": "value"}`

req, err := http.NewRequest("POST", u, strings.NewReader(bodyText))
if err != nil {
log.Fatal(err)
}

req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
go_rfc5849_hmac.PublicKey =
go_rfc5849_hmac.SecretKey =
go_rfc5849_hmac.SignSha1Hmac1(req, bodyText)
res, err := http.DefaultClient.Do(req)

// Set your credentials
go_rfc5849_hmac.PublicKey = "your_consumer_key"
go_rfc5849_hmac.SecretKey = "your_consumer_secret"
go_rfc5849_hmac.Token = "your_token_secret" // Leave empty if not using token

// Sign the request
// Note: bodyText is passed to be included in signature if Content-Type is application/x-www-form-urlencoded
err = go_rfc5849_hmac.SignSha1Hmac1(req, bodyText)
if err != nil {
log.Fatal(err)
}
buff := bytes.NewBuffer(nil)
res.Write(buff)
log.Printf("%s", buff.String())

fmt.Println("Authorization Header:", req.Header.Get("Authorization"))

// Execute request
// client := &http.Client{}
// resp, err := client.Do(req)
// ...
}
```