Skip to content

Commit

Permalink
Ensure we have correct padding everywhere (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler authored Oct 30, 2024
1 parent 982c378 commit bd76e88
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 140 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/api-breakage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: API breaking changes

on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-apibreakage
cancel-in-progress: true

jobs:
linux:
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: swift:latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# https://github.com/actions/checkout/issues/766
- name: Mark the workspace as safe
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: API breaking changes
run: |
swift package diagnose-api-breaking-changes origin/${GITHUB_BASE_REF}
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let package = Package(
.library(name: "SRP", targets: ["SRP"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-crypto", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-crypto", "1.0.0"..<"5.0.0"),
.package(url: "https://github.com/adam-fowler/big-num", from: "2.0.0"),
],
targets: [
Expand Down
10 changes: 10 additions & 0 deletions Sources/SRP/Array.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ extension Array where Element: FixedWidthInteger {
}
}

extension Array where Element == UInt8 {
func pad(to size: Int) -> [UInt8] {
let padSize = size - self.count
guard padSize > 0 else { return self }
// create prefix and return prefix + data
let prefix: [UInt8] = (1...padSize).reduce([]) { result,_ in return result + [0] }
return prefix + self
}
}

/// xor together the contents of two byte arrays
func ^ (lhs: [UInt8], rhs: [UInt8]) -> [UInt8] {
precondition(lhs.count == rhs.count, "Arrays are required to be the same size")
Expand Down
128 changes: 84 additions & 44 deletions Sources/SRP/client.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import BigNum
import Crypto
import Foundation

/// Manages the client side of Secure Remote Password
///
Expand Down Expand Up @@ -27,14 +28,14 @@ public struct SRPClient<H: HashFunction> {
/// Initiate the authentication process
/// - Returns: An authentication state. The A value from this state should be sent to the server
public func generateKeys() -> SRPKeyPair {
var a = BigNum()
var A = BigNum()
var a: BigNum
var A: BigNum
repeat {
a = BigNum(bytes: SymmetricKey(size: .bits256))
A = configuration.g.power(a, modulus: configuration.N)
} while A % configuration.N == BigNum(0)

return SRPKeyPair(public: SRPKey(A), private: SRPKey(a))
return SRPKeyPair(public: SRPKey(A, padding: self.configuration.sizeN), private: SRPKey(a))
}

/// return shared secret given the username, password, B value and salt from the server
Expand All @@ -46,51 +47,63 @@ public struct SRPClient<H: HashFunction> {
/// - serverPublicKey: server public key
/// - Throws: `nullServerKey`
/// - Returns: shared secret
public func calculateSharedSecret(username: String, password: String, salt: [UInt8], clientKeys: SRPKeyPair, serverPublicKey: SRPKey) throws -> SRPKey {
public func calculateSharedSecret(
username: String,
password: String,
salt: [UInt8],
clientKeys: SRPKeyPair,
serverPublicKey: SRPKey
) throws -> SRPKey {
let message = [UInt8]("\(username):\(password)".utf8)
let sharedSecret = try calculateSharedSecret(message: message, salt: salt, clientKeys: clientKeys, serverPublicKey: serverPublicKey)
return SRPKey(sharedSecret)
return try calculateSharedSecret(message: message, salt: salt, clientKeys: clientKeys, serverPublicKey: serverPublicKey)
}


/// calculate proof of shared secret to send to server
/// return shared secret given a binary password, B value and salt from the server
/// - Parameters:
/// - clientPublicKey: client public key
/// - password: password
/// - salt: salt
/// - clientKeys: client public/private keys
/// - serverPublicKey: server public key
/// - sharedSecret: shared secret
/// - Returns: The client verification code which should be passed to the server
public func calculateSimpleClientProof(clientPublicKey: SRPKey, serverPublicKey: SRPKey, sharedSecret: SRPKey) -> [UInt8] {
// get verification code
return SRP<H>.calculateSimpleClientProof(clientPublicKey: clientPublicKey, serverPublicKey: serverPublicKey, sharedSecret: sharedSecret)
/// - Throws: `nullServerKey`
/// - Returns: shared secret
public func calculateSharedSecret(
password: [UInt8],
salt: [UInt8],
clientKeys: SRPKeyPair,
serverPublicKey: SRPKey
) throws -> SRPKey {
let message = [0x3a] + password
return try calculateSharedSecret(message: message, salt: salt, clientKeys: clientKeys, serverPublicKey: serverPublicKey)
}

/// If the server returns that the client verification code was valiid it will also return a server verification code that the client can use to verify the server is correct
///
/// - Parameters:
/// - code: Verification code returned by server
/// - state: Authentication state
/// - Throws: `requiresVerificationKey`, `invalidServerCode`
public func verifySimpleServerProof(serverProof: [UInt8], clientProof: [UInt8], clientKeys: SRPKeyPair, sharedSecret: SRPKey) throws {
// get out version of server proof
let HAMS = SRP<H>.calculateSimpleServerVerification(clientPublicKey: clientKeys.public, clientProof: clientProof, sharedSecret: sharedSecret)
// is it the same
guard serverProof == HAMS else { throw SRPClientError.invalidServerCode }
}

/// calculate proof of shared secret to send to server
/// - Parameters:
/// - username: username
/// - username: Username
/// - salt: The salt value associated with the user returned by the server
/// - clientPublicKey: client public key
/// - clientPublicKey: Client public key
/// - serverPublicKey: server public key
/// - sharedSecret: shared secret
/// - Returns: The client verification code which should be passed to the server
public func calculateClientProof(username: String, salt: [UInt8], clientPublicKey: SRPKey, serverPublicKey: SRPKey, sharedSecret: SRPKey) -> [UInt8] {

public func calculateClientProof(
username: String,
salt: [UInt8],
clientPublicKey: SRPKey,
serverPublicKey: SRPKey,
sharedSecret: SRPKey
) -> [UInt8] {
let clientPublicKey = clientPublicKey.with(padding: self.configuration.sizeN)
let serverPublicKey = serverPublicKey.with(padding: self.configuration.sizeN)
let sharedSecret = sharedSecret.with(padding: self.configuration.sizeN)
let hashSharedSecret = [UInt8](H.hash(data: sharedSecret.bytes))

// get verification code
return SRP<H>.calculateClientProof(configuration: configuration, username: username, salt: salt, clientPublicKey: clientPublicKey, serverPublicKey: serverPublicKey, hashSharedSecret: hashSharedSecret)
return SRP<H>.calculateClientProof(
configuration: configuration,
username: username,
salt: salt,
clientPublicKey: clientPublicKey,
serverPublicKey: serverPublicKey,
hashSharedSecret: hashSharedSecret
)
}

/// If the server returns that the client verification code was valid it will also return a server
Expand All @@ -101,24 +114,39 @@ public struct SRPClient<H: HashFunction> {
/// - clientPublicKey: Client public key
/// - clientProof: Client proof
/// - sharedSecret: Shared secret
public func calculateServerProof(clientPublicKey: SRPKey, clientProof: [UInt8], sharedSecret: SRPKey) -> [UInt8] {
public func calculateServerProof(
clientPublicKey: SRPKey,
clientProof: [UInt8],
sharedSecret: SRPKey
) -> [UInt8] {
let clientPublicKey = clientPublicKey.with(padding: self.configuration.sizeN)
let sharedSecret = sharedSecret.with(padding: self.configuration.sizeN)
let hashSharedSecret = [UInt8](H.hash(data: sharedSecret.bytes))
// get out version of server proof
return SRP<H>.calculateServerVerification(clientPublicKey: clientPublicKey, clientProof: clientProof, sharedSecret: hashSharedSecret)
return SRP<H>.calculateServerVerification(
clientPublicKey: clientPublicKey,
clientProof: clientProof,
hashSharedSecret: hashSharedSecret
)
}

/// If the server returns that the client verification code was valid it will also return a server
/// verification code that the client can use to verify the server is correct
///
/// - Parameters:
/// - clientProof: Server proof
/// - serverProof: Server proof
/// - clientProof: Client proof
/// - clientKeys: Client keys
/// - clientPublicKey: Client public key
/// - sharedSecret: Shared secret
/// - Throws: `requiresVerificationKey`, `invalidServerCode`
public func verifyServerProof(serverProof: [UInt8], clientProof: [UInt8], clientKeys: SRPKeyPair, sharedSecret: SRPKey) throws {
public func verifyServerProof(
serverProof: [UInt8],
clientProof: [UInt8],
clientPublicKey: SRPKey,
sharedSecret: SRPKey
) throws {
// get our version of server proof
let HAMK = calculateServerProof(clientPublicKey: clientKeys.public, clientProof: clientProof, sharedSecret: sharedSecret)
let HAMK = calculateServerProof(clientPublicKey: clientPublicKey, clientProof: clientProof, sharedSecret: sharedSecret)
// is it the same
guard serverProof == HAMK else { throw SRPClientError.invalidServerCode }
}
Expand All @@ -134,17 +162,29 @@ public struct SRPClient<H: HashFunction> {
public func generateSaltAndVerifier(username: String, password: String) -> (salt: [UInt8], verifier: SRPKey) {
let salt = [UInt8].random(count: 16)
let verifier = generatePasswordVerifier(username: username, password: password, salt: salt)
return (salt: salt, verifier: SRPKey(verifier))
return (salt: salt, verifier: SRPKey(verifier, padding: configuration.sizeN))
}

/// Hash data using same hash function that SRP uses
/// - Parameter data: Data to be hashed
/// - Returns: Hashed data
@inlinable public func hash<D>(data: D) -> H.Digest where D : DataProtocol {
H.hash(data: data)
}
}

extension SRPClient {
/// return shared secret given the username, password, B value and salt from the server
func calculateSharedSecret(message: [UInt8], salt: [UInt8], clientKeys: SRPKeyPair, serverPublicKey: SRPKey) throws -> BigNum {
/// return shared secret given the message (username:password), salt from server, client keys, and B value
func calculateSharedSecret(
message: [UInt8],
salt: [UInt8],
clientKeys: SRPKeyPair,
serverPublicKey: SRPKey
) throws -> SRPKey {
guard serverPublicKey.number % configuration.N != BigNum(0) else { throw SRPClientError.nullServerKey }

// calculate u = H(clientPublicKey | serverPublicKey)
let u = SRP<H>.calculateU(clientPublicKey: clientKeys.public.bytes, serverPublicKey: serverPublicKey.bytes, pad: configuration.sizeN)
let u = SRP<H>.calculateU(clientPublicKey: clientKeys.public.bytes, serverPublicKey: serverPublicKey.bytes)

guard u != 0 else { throw SRPClientError.nullServerKey }

Expand All @@ -153,7 +193,7 @@ extension SRPClient {
// calculate S = (B - k*g^x)^(a+u*x)
let S = (serverPublicKey.number - configuration.k * configuration.g.power(x, modulus: configuration.N)).power(clientKeys.private.number + u * x, modulus: configuration.N)

return S
return .init(S, padding: self.configuration.sizeN)
}

/// generate password verifier
Expand Down
4 changes: 2 additions & 2 deletions Sources/SRP/configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public struct SRPConfiguration<H: HashFunction> {
self.N = prime.group
self.sizeN = Int(self.N.numBits() + 7) / 8
self.g = prime.generator
self.k = BigNum(bytes: [UInt8](H.hash(data: self.N.bytes + SRP<H>.pad(self.g.bytes, to: sizeN))))
self.k = BigNum(bytes: [UInt8](H.hash(data: self.N.bytes + self.g.bytes.pad(to: sizeN))))
}

/// Initialise SRPConfiguration with your own prime and multiplicative group generator
Expand All @@ -29,7 +29,7 @@ public struct SRPConfiguration<H: HashFunction> {
self.N = N
self.sizeN = Int(self.N.numBits() + 7) / 8
self.g = g
self.k = BigNum(bytes: [UInt8](H.hash(data: self.N.bytes + SRP<H>.pad(self.g.bytes, to: sizeN))))
self.k = BigNum(bytes: [UInt8](H.hash(data: self.N.bytes + self.g.bytes.pad(to: sizeN))))
}

public enum Prime {
Expand Down
39 changes: 31 additions & 8 deletions Sources/SRP/keys.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
import BigNum
import Crypto
import Foundation

/// Wrapper for keys used by SRP
public struct SRPKey {
/// SRPKey internal storage
public let number: BigNum
public var bytes: [UInt8] { number.bytes }
public var hex: String { number.hex }
/// padding
public let padding: Int
/// Representation as a byte array
public var bytes: [UInt8] { number.bytes.pad(to: self.padding) }
/// Representation as a hex string
public var hex: String { number.bytes.pad(to: self.padding).hexdigest() }

public init(_ bytes: [UInt8]) {
/// Initialize with an array of bytes
@inlinable public init<C: Collection & ContiguousBytes>(_ bytes: C, padding: Int? = nil) {
self.number = BigNum(bytes: bytes)
self.padding = padding ?? bytes.count
}

public init(_ number: BigNum) {
self.number = number
/// Initialize with a crypto digest
@inlinable public init<D: Digest>(_ digest: D, padding: Int? = nil) {
self.number = BigNum(bytes: digest)
self.padding = padding ?? D.byteCount
}

public init?(hex: String) {
/// Initialize with a hex string
@inlinable public init?(hex: String, padding: Int = 0) {
guard let number = BigNum(hex: hex) else { return nil }
self.number = number
self.padding = padding
}

/// Initialize with a BigNum
@usableFromInline init(_ number: BigNum, padding: Int = 0) {
self.number = number
self.padding = padding
}

/// Return SRPKey with padding
func with(padding: Int) -> SRPKey {
.init(self.number, padding: padding)
}
}

Expand All @@ -27,12 +51,11 @@ public struct SRPKeyPair {
public let `public`: SRPKey
public let `private`: SRPKey


/// Initialise a SRPKeyPair object
/// - Parameters:
/// - public: The public key of the key pair
/// - private: The private key of the key pair
public init(`public`: SRPKey, `private`: SRPKey) {
init(`public`: SRPKey, `private`: SRPKey) {
self.private = `private`
self.public = `public`
}
Expand Down
Loading

0 comments on commit bd76e88

Please sign in to comment.